UNPKG

commit-story

Version:

Automated Git Journal System with AI Assistant Context Integration

866 lines (789 loc) 35.7 kB
/** * OpenTelemetry Standards Module * * Centralizes OpenTelemetry semantic conventions and provides builders to enforce * correct attribute naming, span patterns, and metric emission. Prevents instrumentation * errors and ensures consistency across the entire codebase. * * Semantic Namespace Guidelines: * - gen_ai.*: Direct AI operation characteristics (model params, tokens, response metrics) * - commit_story.*: Application-specific attributes (business logic, infrastructure) * * Metric Conventions: * - Use hierarchical naming: commit_story.category.metric_name * - Follow OpenTelemetry units: milliseconds for duration, dimensionless "1" for counts * - Gauge: Point-in-time values (message counts, current state) * - Counter: Incrementing values (total operations, cumulative errors) * - Histogram: Distribution data (durations, payload sizes) */ import { metrics } from '@opentelemetry/api'; // OpenTelemetry semantic convention constants (v1.37.0) // Defined inline to avoid 10MB @opentelemetry/semantic-conventions dependency // Spec: https://opentelemetry.io/docs/specs/semconv/ const SEMATTRS_CODE_FUNCTION = 'code.function'; const SEMATTRS_CODE_FILEPATH = 'code.filepath'; const SEMATTRS_CODE_LINENO = 'code.lineno'; const SEMATTRS_RPC_SYSTEM = 'rpc.system'; const SEMATTRS_RPC_SERVICE = 'rpc.service'; const SEMATTRS_RPC_METHOD = 'rpc.method'; /** * Detects AI provider from model name for telemetry * @param {string} modelName - The model name (e.g., 'gpt-4o-mini', 'claude-3') * @returns {string} Provider name ('openai', 'anthropic', 'google', 'meta', 'unknown') */ export function getProviderFromModel(modelName) { if (!modelName) return 'unknown'; const model = modelName.toLowerCase(); if (model.startsWith('gpt')) return 'openai'; if (model.includes('claude')) return 'anthropic'; if (model.includes('gemini')) return 'google'; if (model.includes('llama')) return 'meta'; return 'unknown'; } /** * Get OpenTelemetry Meter instance for commit-story metrics * Follows OpenTelemetry best practices for meter naming and versioning */ function getMeter() { return metrics.getMeter('commit-story', '1.0.0'); } /** * OpenTelemetry standards constant - centralized patterns and conventions */ export const OTEL = { // Application namespace for custom attributes NAMESPACE: 'commit_story', // Span name builders (enforce correct naming patterns) span: { main: () => 'commit_story.main', // Application infrastructure operations connectivity: () => 'commit_story.connectivity_test', // Context and data operations context: { gather: () => 'context.gather_for_commit', filter: () => 'context.filter_messages', extract_text: () => 'context.extract_text_from_messages', calculate_metadata: () => 'context.calculate_chat_metadata' }, // Journal generation operations journal: { generate: () => 'journal.generate_entry', save: () => 'journal.save_entry', format: () => 'journal.format_entry', discover_reflections: () => 'journal.discover_reflections', parse_reflection_file: () => 'journal.parse_reflection_file', parse_timestamp: () => 'journal.parse_timestamp', timezone_offset: () => 'journal.timezone_offset', get_file_path: () => 'journal.get_file_path' }, // AI generation operations ai: { summary: () => 'summary.generate', dialogue: () => 'dialogue.generate', technical: () => 'technical_decisions.generate' }, // Prompt construction operations prompts: { summary: () => 'prompts.summary_construction', dialogue: () => 'prompts.dialogue_construction', technical: () => 'prompts.technical_construction' }, // Data collection operations collectors: { claude: () => 'claude.collect_messages', git: () => 'git.collect_data' }, // CLI argument parsing operations cli: { parse_arguments: () => 'cli.parse_arguments', process_flags: () => 'cli.process_flags' }, // Application initialization operations initialization: { conditional: () => 'initialization.conditional_setup', telemetry: () => 'initialization.telemetry_setup', logging: () => 'initialization.logging_setup' }, // Application shutdown operations shutdown: { graceful: () => 'shutdown.graceful_shutdown', flush_logs: () => 'shutdown.flush_logs', flush_metrics: () => 'shutdown.flush_metrics' }, // Data filtering operations filters: { sensitiveData: () => 'filters.redact_sensitive_data' }, // Utility operations utils: { contextSelect: () => 'utils.select_context', sessionFormat: () => 'utils.session_format', commitContentAnalyzer: () => 'utils.commit_content_analyzer.analyze', journal_paths: { generate_path: () => 'utils.journal_paths.generate_path', create_directory: () => 'utils.journal_paths.create_directory', format_date: () => 'utils.journal_paths.format_date', format_timestamp: () => 'utils.journal_paths.format_timestamp', write_file: () => 'utils.journal_paths.write_file' } }, // Claude collector utility operations claude: { find_files: () => 'claude.find_files', group_by_session: () => 'claude.group_by_session' }, // MCP (Model Context Protocol) operations mcp: { server_startup: () => 'mcp.server_startup', server_shutdown: () => 'mcp.server_shutdown', tool_invocation: () => 'mcp.tool_invocation', tool: { journal_add_reflection: () => 'mcp.tool.journal_add_reflection' } } }, // Attribute builders (enforce correct conventions) attrs: { /** * Official OpenTelemetry GenAI attributes * Based on: https://opentelemetry.io/docs/specs/semconv/gen-ai/ */ genAI: { /** * Request attributes for AI operations * @param {string} model - Model name * @param {number} temperature - Generation temperature * @param {number} msgCount - Number of messages sent to AI * @returns {Object} Official GenAI request attributes */ request: (model, temperature, msgCount) => ({ 'gen_ai.request.model': model, 'gen_ai.request.temperature': temperature, 'gen_ai.request.messages_count': msgCount, // Extension: AI-specific metric 'gen_ai.operation.name': 'chat', 'gen_ai.provider.name': getProviderFromModel(model) }), /** * Usage/response attributes for AI operations * @param {Object} response - AI response object * @returns {Object} Official GenAI usage attributes */ usage: (response) => ({ 'gen_ai.response.model': response.model, 'gen_ai.response.message_length': response.content?.length || 0, // Extension: AI response characteristic 'gen_ai.usage.prompt_tokens': response.usage?.prompt_tokens || 0, 'gen_ai.usage.completion_tokens': response.usage?.completion_tokens || 0 }), /** * Conversation tracking attributes * @param {string} conversationId - Unique conversation ID * @returns {Object} Conversation attributes */ conversation: (conversationId) => ({ 'gen_ai.conversation.id': conversationId }) }, /** * Application-specific commit attributes * @param {Object} commitData - Commit information * @returns {Object} Commit attributes with commit_story namespace */ commit: (commitData) => ({ [`${OTEL.NAMESPACE}.commit.hash`]: commitData.hash, [`${OTEL.NAMESPACE}.commit.message`]: commitData.message?.split('\n')[0], // First line only [`${OTEL.NAMESPACE}.commit.timestamp`]: commitData.timestamp?.toISOString(), [`${OTEL.NAMESPACE}.commit.author`]: commitData.author, [`${OTEL.NAMESPACE}.commit.ref`]: commitData.ref }), /** * Application chat context attributes * @param {Object} chatData - Chat statistics * @returns {Object} Chat attributes with commit_story namespace */ chat: (chatData) => ({ [`${OTEL.NAMESPACE}.chat.messages_count`]: chatData.filtered || chatData.count, [`${OTEL.NAMESPACE}.chat.raw_messages_count`]: chatData.raw, [`${OTEL.NAMESPACE}.chat.total_messages`]: chatData.total, [`${OTEL.NAMESPACE}.chat.user_messages`]: chatData.userMessages, [`${OTEL.NAMESPACE}.chat.assistant_messages`]: chatData.assistantMessages, [`${OTEL.NAMESPACE}.chat.user_messages_over_twenty`]: chatData.userMessagesOverTwenty }), /** * Prompt construction attributes * @param {Object} promptData - Prompt construction parameters * @returns {Object} Prompt attributes with commit_story namespace */ prompts: { /** * Summary prompt construction attributes * @param {Object} params - Summary prompt parameters * @returns {Object} Summary prompt attributes */ summary: (params) => ({ [`${OTEL.NAMESPACE}.prompt.has_functional_code`]: params.hasFunctionalCode, [`${OTEL.NAMESPACE}.prompt.has_substantial_chat`]: params.hasSubstantialChat, [`${OTEL.NAMESPACE}.prompt.scenario`]: params.scenario, [`${OTEL.NAMESPACE}.prompt.length`]: params.length }) }, /** * File analysis attributes * @param {Object} fileData - File analysis results * @returns {Object} File attributes with commit_story namespace */ files: (fileData) => ({ [`${OTEL.NAMESPACE}.files.total`]: fileData.total, [`${OTEL.NAMESPACE}.files.documentation`]: fileData.documentation, [`${OTEL.NAMESPACE}.files.functional`]: fileData.functional, [`${OTEL.NAMESPACE}.files.has_functional_code`]: fileData.hasFunctionalCode, [`${OTEL.NAMESPACE}.files.only_documentation`]: fileData.onlyDocumentation }), /** * Context processing attributes * @param {Object} contextData - Context processing metrics * @returns {Object} Context attributes */ context: (contextData) => ({ [`${OTEL.NAMESPACE}.context.original_messages`]: contextData.originalCount, [`${OTEL.NAMESPACE}.context.filtered_messages`]: contextData.filteredCount, [`${OTEL.NAMESPACE}.context.removed_messages`]: contextData.removedCount, [`${OTEL.NAMESPACE}.context.token_reduction`]: contextData.tokensSaved, [`${OTEL.NAMESPACE}.context.token_reduction_percent`]: contextData.reductionPercent, [`${OTEL.NAMESPACE}.context.original_chat_tokens`]: contextData.originalChatTokens, [`${OTEL.NAMESPACE}.context.filtered_chat_tokens`]: contextData.filteredChatTokens, [`${OTEL.NAMESPACE}.context.diff_tokens`]: contextData.diffTokens, [`${OTEL.NAMESPACE}.context.total_estimated_tokens`]: contextData.totalTokens, [`${OTEL.NAMESPACE}.context.final_messages`]: contextData.finalMessages, [`${OTEL.NAMESPACE}.context.final_chat_tokens`]: contextData.finalChatTokens, [`${OTEL.NAMESPACE}.context.aggressive_filtering`]: contextData.aggressiveFiltering }), /** * Text extraction operation attributes * @param {Object} textData - Text extraction metrics * @returns {Object} Text extraction attributes */ textExtraction: (textData) => ({ [`${OTEL.NAMESPACE}.text.input_messages`]: textData.inputMessages, [`${OTEL.NAMESPACE}.text.processed_messages`]: textData.processedMessages, [`${OTEL.NAMESPACE}.text.string_content_messages`]: textData.stringContentMessages, [`${OTEL.NAMESPACE}.text.array_content_messages`]: textData.arrayContentMessages, [`${OTEL.NAMESPACE}.text.unknown_content_messages`]: textData.unknownContentMessages, [`${OTEL.NAMESPACE}.text.empty_content_messages`]: textData.emptyContentMessages, [`${OTEL.NAMESPACE}.text.total_content_length`]: textData.totalContentLength, [`${OTEL.NAMESPACE}.text.average_content_length`]: textData.averageContentLength, [`${OTEL.NAMESPACE}.text.processing_duration_ms`]: textData.processingDuration }), /** * Chat metadata calculation attributes * @param {Object} metadataData - Metadata calculation metrics * @returns {Object} Metadata calculation attributes */ chatMetadata: (metadataData) => ({ [`${OTEL.NAMESPACE}.metadata.input_messages`]: metadataData.inputMessages, [`${OTEL.NAMESPACE}.metadata.user_messages`]: metadataData.userMessages, [`${OTEL.NAMESPACE}.metadata.assistant_messages`]: metadataData.assistantMessages, [`${OTEL.NAMESPACE}.metadata.over_twenty_char_messages`]: metadataData.overTwentyCharMessages, [`${OTEL.NAMESPACE}.metadata.user_avg_length`]: metadataData.userAvgLength, [`${OTEL.NAMESPACE}.metadata.user_max_length`]: metadataData.userMaxLength, [`${OTEL.NAMESPACE}.metadata.assistant_avg_length`]: metadataData.assistantAvgLength, [`${OTEL.NAMESPACE}.metadata.assistant_max_length`]: metadataData.assistantMaxLength, [`${OTEL.NAMESPACE}.metadata.calculation_duration_ms`]: metadataData.calculationDuration }), /** * Git data collection attributes * @param {Object} gitData - Git data collection metrics * @returns {Object} Git attributes */ gitCollection: (gitData) => ({ [`${OTEL.NAMESPACE}.git.commit_ref`]: gitData.commitRef, [`${OTEL.NAMESPACE}.git.command`]: gitData.command, [`${OTEL.NAMESPACE}.git.previous_commit_found`]: gitData.previousCommitFound, [`${OTEL.NAMESPACE}.git.previous_commit_hash`]: gitData.previousCommitHash, [`${OTEL.NAMESPACE}.git.previous_commit_timestamp`]: gitData.previousCommitTimestamp, [`${OTEL.NAMESPACE}.git.execution_duration_ms`]: gitData.executionDuration }), /** * Journal section length attributes * @param {Object} sectionLengths - Length of each journal section * @returns {Object} Section attributes */ sections: (sectionLengths) => ({ [`${OTEL.NAMESPACE}.sections.summary_length`]: sectionLengths.summary, [`${OTEL.NAMESPACE}.sections.dialogue_length`]: sectionLengths.dialogue, [`${OTEL.NAMESPACE}.sections.technical_decisions_length`]: sectionLengths.technical, [`${OTEL.NAMESPACE}.sections.commit_details_length`]: sectionLengths.details }), /** * Repository and environment attributes * @param {Object} repoData - Repository information * @returns {Object} Repository attributes */ repository: (repoData) => ({ [`${OTEL.NAMESPACE}.repository.path`]: repoData.path, [`${OTEL.NAMESPACE}.repository.name`]: repoData.name }), /** * Journal operation attributes */ journal: { /** * Journal completion attributes (from main execution) * @param {Object} journalData - Journal completion information * @returns {Object} Journal attributes */ completion: (journalData) => ({ [`${OTEL.NAMESPACE}.journal.file_path`]: journalData.filePath, [`${OTEL.NAMESPACE}.journal.completed`]: journalData.completed }), /** * Journal save operation attributes * @param {Object} saveData - Journal save operation data * @returns {Object} Journal save attributes */ save: (saveData) => ({ [`${OTEL.NAMESPACE}.journal.file_path`]: saveData.filePath, [`${OTEL.NAMESPACE}.journal.entry_size`]: saveData.entrySize, [`${OTEL.NAMESPACE}.journal.directory_created`]: saveData.dirCreated, [`${OTEL.NAMESPACE}.journal.write_duration_ms`]: saveData.writeDuration }), /** * Journal format operation attributes * @param {Object} formatData - Journal format operation data * @returns {Object} Journal format attributes */ format: (formatData) => ({ [`${OTEL.NAMESPACE}.journal.entry_size`]: formatData.entrySize, [`${OTEL.NAMESPACE}.journal.reflection_count`]: formatData.reflectionCount, [`${OTEL.NAMESPACE}.journal.section_count`]: formatData.sectionCount, [`${OTEL.NAMESPACE}.journal.format_duration_ms`]: formatData.formatDuration }), /** * Journal reflection discovery attributes * @param {Object} discoveryData - Reflection discovery operation data * @returns {Object} Discovery attributes */ discovery: (discoveryData) => ({ [`${OTEL.NAMESPACE}.journal.files_checked`]: discoveryData.filesChecked, [`${OTEL.NAMESPACE}.journal.reflections_found`]: discoveryData.reflectionsFound, [`${OTEL.NAMESPACE}.journal.time_window_hours`]: discoveryData.timeWindowHours, [`${OTEL.NAMESPACE}.journal.discovery_duration_ms`]: discoveryData.discoveryDuration }), /** * Journal reflection file parsing attributes * @param {Object} parseData - File parsing operation data * @returns {Object} Parse attributes */ parse: (parseData) => ({ [`${OTEL.NAMESPACE}.journal.file_size`]: parseData.fileSize, [`${OTEL.NAMESPACE}.journal.lines_parsed`]: parseData.linesParsed, [`${OTEL.NAMESPACE}.journal.entries_extracted`]: parseData.entriesExtracted, [`${OTEL.NAMESPACE}.journal.parse_duration_ms`]: parseData.parseDuration, [SEMATTRS_CODE_FILEPATH]: parseData.filePath }), /** * Journal timestamp parsing attributes * @param {Object} timestampData - Timestamp parsing data * @returns {Object} Timestamp attributes */ timestamp: (timestampData) => ({ [`${OTEL.NAMESPACE}.journal.timestamp_format`]: timestampData.format, [`${OTEL.NAMESPACE}.journal.timezone_detected`]: timestampData.timezone, [`${OTEL.NAMESPACE}.journal.parse_success`]: timestampData.parseSuccess }), /** * Journal file path generation attributes * @param {Object} pathData - Path generation data * @returns {Object} Path attributes */ path: (pathData) => ({ [`${OTEL.NAMESPACE}.journal.requested_date`]: pathData.requestedDate, [`${OTEL.NAMESPACE}.journal.generated_path`]: pathData.generatedPath, [SEMATTRS_CODE_FILEPATH]: pathData.generatedPath }), /** * Timezone offset calculation attributes * @param {Object} timezoneData - Timezone offset calculation data * @returns {Object} Timezone offset attributes */ timezoneOffset: (timezoneData) => ({ [`${OTEL.NAMESPACE}.timezone.input_utc_millis`]: timezoneData.inputUtcMillis, [`${OTEL.NAMESPACE}.timezone.iana_zone`]: timezoneData.ianaZone, [`${OTEL.NAMESPACE}.timezone.offset_minutes`]: timezoneData.offsetMinutes, [`${OTEL.NAMESPACE}.timezone.calculation_duration_ms`]: timezoneData.calculationDuration }) }, /** * CLI argument parsing operation attributes */ cli: { /** * CLI argument parsing attributes * @param {Object} parseData - CLI parsing operation data * @returns {Object} CLI parsing attributes */ parseArguments: (parseData) => ({ [`${OTEL.NAMESPACE}.cli.total_arguments`]: parseData.totalArguments, [`${OTEL.NAMESPACE}.cli.processed_arguments`]: parseData.processedArguments, [`${OTEL.NAMESPACE}.cli.dry_run_flag`]: parseData.dryRunFlag, [`${OTEL.NAMESPACE}.cli.commit_ref_provided`]: parseData.commitRefProvided, [`${OTEL.NAMESPACE}.cli.commit_ref`]: parseData.commitRef, [`${OTEL.NAMESPACE}.cli.unknown_flags`]: parseData.unknownFlags, [`${OTEL.NAMESPACE}.cli.parsing_duration_ms`]: parseData.parsingDuration }), /** * CLI flag processing attributes * @param {Object} flagData - CLI flag processing data * @returns {Object} CLI flag attributes */ processFlags: (flagData) => ({ [`${OTEL.NAMESPACE}.cli.flags_found`]: flagData.flagsFound, [`${OTEL.NAMESPACE}.cli.flags_processed`]: flagData.flagsProcessed, [`${OTEL.NAMESPACE}.cli.test_flag_alias`]: flagData.testFlagAlias, [`${OTEL.NAMESPACE}.cli.processing_duration_ms`]: flagData.processingDuration }) }, /** * Application initialization operation attributes */ initialization: { /** * Conditional initialization attributes * @param {Object} initData - Initialization operation data * @returns {Object} Initialization attributes */ conditional: (initData) => ({ [`${OTEL.NAMESPACE}.init.condition_met`]: initData.conditionMet, [`${OTEL.NAMESPACE}.init.condition_type`]: initData.conditionType, [`${OTEL.NAMESPACE}.init.initialization_duration_ms`]: initData.initializationDuration, [`${OTEL.NAMESPACE}.init.skip_reason`]: initData.skipReason }), /** * Telemetry initialization attributes * @param {Object} telemetryData - Telemetry initialization data * @returns {Object} Telemetry initialization attributes */ telemetry: (telemetryData) => ({ [`${OTEL.NAMESPACE}.telemetry.sdk_initialized`]: telemetryData.sdkInitialized, [`${OTEL.NAMESPACE}.telemetry.service_name`]: telemetryData.serviceName, [`${OTEL.NAMESPACE}.telemetry.service_version`]: telemetryData.serviceVersion, [`${OTEL.NAMESPACE}.telemetry.otlp_endpoint`]: telemetryData.otlpEndpoint, [`${OTEL.NAMESPACE}.telemetry.console_output`]: telemetryData.consoleOutput, [`${OTEL.NAMESPACE}.telemetry.initialization_duration_ms`]: telemetryData.initializationDuration }), /** * Logging initialization attributes * @param {Object} loggingData - Logging initialization data * @returns {Object} Logging initialization attributes */ logging: (loggingData) => ({ [`${OTEL.NAMESPACE}.logging.provider_initialized`]: loggingData.providerInitialized, [`${OTEL.NAMESPACE}.logging.batch_processor`]: loggingData.batchProcessor, [`${OTEL.NAMESPACE}.logging.otlp_endpoint`]: loggingData.otlpEndpoint, [`${OTEL.NAMESPACE}.logging.max_batch_size`]: loggingData.maxBatchSize, [`${OTEL.NAMESPACE}.logging.scheduled_delay_ms`]: loggingData.scheduledDelayMs, [`${OTEL.NAMESPACE}.logging.initialization_duration_ms`]: loggingData.initializationDuration }) }, /** * Application shutdown operation attributes */ shutdown: { /** * Graceful shutdown attributes * @param {Object} shutdownData - Shutdown operation data * @returns {Object} Shutdown attributes */ graceful: (shutdownData) => ({ [`${OTEL.NAMESPACE}.shutdown.triggered_by`]: shutdownData.triggeredBy, [`${OTEL.NAMESPACE}.shutdown.logs_flushed`]: shutdownData.logsFlushed, [`${OTEL.NAMESPACE}.shutdown.metrics_flushed`]: shutdownData.metricsFlushed, [`${OTEL.NAMESPACE}.shutdown.sdk_shutdown`]: shutdownData.sdkShutdown, [`${OTEL.NAMESPACE}.shutdown.total_duration_ms`]: shutdownData.totalDuration, [`${OTEL.NAMESPACE}.shutdown.errors_encountered`]: shutdownData.errorsEncountered }), /** * Log flushing attributes * @param {Object} flushData - Log flush operation data * @returns {Object} Log flush attributes */ flushLogs: (flushData) => ({ [`${OTEL.NAMESPACE}.shutdown.logs_pending`]: flushData.logsPending, [`${OTEL.NAMESPACE}.shutdown.logs_flushed_count`]: flushData.logsFlushedCount, [`${OTEL.NAMESPACE}.shutdown.flush_logs_duration_ms`]: flushData.flushLogsDuration, [`${OTEL.NAMESPACE}.shutdown.flush_logs_success`]: flushData.flushLogsSuccess }), /** * Metrics flushing attributes * @param {Object} flushData - Metrics flush operation data * @returns {Object} Metrics flush attributes */ flushMetrics: (flushData) => ({ [`${OTEL.NAMESPACE}.shutdown.metrics_pending`]: flushData.metricsPending, [`${OTEL.NAMESPACE}.shutdown.metrics_flushed_count`]: flushData.metricsFlushedCount, [`${OTEL.NAMESPACE}.shutdown.flush_metrics_duration_ms`]: flushData.flushMetricsDuration, [`${OTEL.NAMESPACE}.shutdown.flush_metrics_success`]: flushData.flushMetricsSuccess }) }, /** * Data filtering operation attributes */ filters: { /** * Sensitive data redaction attributes (NO sensitive data captured, only counts) * @param {Object} filterData - Filter operation metrics * @returns {Object} Filter attributes (counts and performance only) */ sensitiveData: (filterData) => ({ [`${OTEL.NAMESPACE}.filter.input_length`]: filterData.inputLength, [`${OTEL.NAMESPACE}.filter.output_length`]: filterData.outputLength, [`${OTEL.NAMESPACE}.filter.keys_redacted`]: filterData.keysRedacted, [`${OTEL.NAMESPACE}.filter.jwts_redacted`]: filterData.jwtsRedacted, [`${OTEL.NAMESPACE}.filter.tokens_redacted`]: filterData.tokensRedacted, [`${OTEL.NAMESPACE}.filter.emails_redacted`]: filterData.emailsRedacted, [`${OTEL.NAMESPACE}.filter.total_redactions`]: filterData.totalRedactions, [`${OTEL.NAMESPACE}.filter.processing_duration_ms`]: filterData.processingDuration }) }, /** * Utility function operation attributes */ utils: { /** * Context selection operation attributes * @param {Object} selectionData - Context selection metrics * @returns {Object} Context selection attributes */ contextSelect: (selectionData) => ({ [`${OTEL.NAMESPACE}.utils.selections_requested`]: selectionData.selectionsRequested, [`${OTEL.NAMESPACE}.utils.selections_found`]: selectionData.selectionsFound, [`${OTEL.NAMESPACE}.utils.description_length`]: selectionData.descriptionLength, [`${OTEL.NAMESPACE}.utils.data_keys`]: selectionData.dataKeys, [`${OTEL.NAMESPACE}.utils.processing_duration_ms`]: selectionData.processingDuration }), /** * Session formatting operation attributes * @param {Object} formatData - Session format metrics * @returns {Object} Session format attributes */ sessionFormat: (formatData) => ({ [`${OTEL.NAMESPACE}.session.input_sessions`]: formatData.inputSessions, [`${OTEL.NAMESPACE}.session.formatted_sessions`]: formatData.formattedSessions, [`${OTEL.NAMESPACE}.session.total_messages`]: formatData.totalMessages, [`${OTEL.NAMESPACE}.session.processing_duration_ms`]: formatData.processingDuration }), /** * Journal paths operation attributes * @param {Object} pathData - Path operation data * @returns {Object} Journal paths attributes */ journalPaths: { generatePath: (pathData) => ({ [`${OTEL.NAMESPACE}.journal.type`]: pathData.type, [`${OTEL.NAMESPACE}.path.month_dir`]: pathData.monthDir, [`${OTEL.NAMESPACE}.path.file_name`]: pathData.fileName, [`${OTEL.NAMESPACE}.path.full_path`]: pathData.fullPath, [SEMATTRS_CODE_FILEPATH]: pathData.fullPath // OpenTelemetry semantic convention }), createDirectory: (directoryData) => ({ [`${OTEL.NAMESPACE}.directory.path`]: directoryData.path, [`${OTEL.NAMESPACE}.directory.type`]: directoryData.type, [`${OTEL.NAMESPACE}.directory.created`]: directoryData.created, [`${OTEL.NAMESPACE}.directory.operation_duration_ms`]: directoryData.operationDuration, [SEMATTRS_CODE_FILEPATH]: directoryData.path // OpenTelemetry semantic convention (directory) }), formatDate: (dateData) => ({ [`${OTEL.NAMESPACE}.date.year`]: dateData.year, [`${OTEL.NAMESPACE}.date.month`]: dateData.month, [`${OTEL.NAMESPACE}.date.day`]: dateData.day, [`${OTEL.NAMESPACE}.path.month_dir`]: dateData.monthDir, [`${OTEL.NAMESPACE}.path.file_name`]: dateData.fileName }), formatTimestamp: (timestampData) => ({ [`${OTEL.NAMESPACE}.timestamp.formatted`]: timestampData.formatted, [`${OTEL.NAMESPACE}.timestamp.timezone`]: timestampData.timezone }) } }, /** * Claude collector utility operation attributes */ claude: { /** * File discovery operation attributes * @param {Object} discoveryData - File discovery metrics * @returns {Object} File discovery attributes */ findFiles: (discoveryData) => ({ [`${OTEL.NAMESPACE}.claude.directories_scanned`]: discoveryData.directoriesScanned, [`${OTEL.NAMESPACE}.claude.files_found`]: discoveryData.filesFound, [`${OTEL.NAMESPACE}.claude.scan_duration_ms`]: discoveryData.scanDuration, [`${OTEL.NAMESPACE}.claude.scan_errors`]: discoveryData.scanErrors }), /** * Message grouping operation attributes * @param {Object} groupData - Grouping operation metrics * @returns {Object} Message grouping attributes */ groupBySession: (groupData) => ({ [`${OTEL.NAMESPACE}.claude.input_messages`]: groupData.inputMessages, [`${OTEL.NAMESPACE}.claude.unique_sessions`]: groupData.uniqueSessions, [`${OTEL.NAMESPACE}.claude.grouped_sessions`]: groupData.groupedSessions, [`${OTEL.NAMESPACE}.claude.grouping_duration_ms`]: groupData.groupingDuration }) }, /** * MCP (Model Context Protocol) operation attributes */ mcp: { /** * MCP server attributes following RPC semantic conventions * @param {Object} serverData - Server operation data * @param {Object} serverData.transport - Transport type (e.g., 'stdio') * @param {Object} serverData.version - MCP version * @param {Object} serverData.method - RPC method name (for JSON-RPC semantic conventions) * @returns {Object} MCP server attributes with proper semantic conventions */ server: (serverData) => ({ [`${OTEL.NAMESPACE}.mcp.transport`]: serverData.transport, [`${OTEL.NAMESPACE}.mcp.version`]: serverData.version, [SEMATTRS_RPC_SYSTEM]: 'jsonrpc', // OpenTelemetry RPC semantic convention [SEMATTRS_RPC_SERVICE]: 'mcp_server', [SEMATTRS_RPC_METHOD]: serverData.method // Tool-specific method name for better AI assistant querying }), /** * MCP tool invocation attributes * @param {Object} toolData - Tool operation data * @returns {Object} Tool attributes with function semantic conventions */ tool: (toolData) => ({ [`${OTEL.NAMESPACE}.mcp.tool_name`]: toolData.name, [`${OTEL.NAMESPACE}.mcp.tool_parameters_count`]: toolData.paramCount, [`${OTEL.NAMESPACE}.mcp.tool_execution_duration_ms`]: toolData.executionDuration, [SEMATTRS_CODE_FUNCTION]: toolData.name // OpenTelemetry code semantic convention }), /** * Reflection-specific attributes * @param {Object} reflectionData - Reflection operation data * @returns {Object} Reflection attributes with file semantic conventions */ reflection: (reflectionData) => ({ [`${OTEL.NAMESPACE}.reflection.text_length`]: reflectionData.textLength, [`${OTEL.NAMESPACE}.reflection.timestamp`]: reflectionData.timestamp, [`${OTEL.NAMESPACE}.reflection.file_created`]: reflectionData.fileCreated, [SEMATTRS_CODE_FILEPATH]: reflectionData.filePath, // OpenTelemetry file semantic convention [`${OTEL.NAMESPACE}.reflection.directory`]: reflectionData.directory }) } }, // Event builders for structured events events: { /** * GenAI prompt event * @param {Array} messages - Messages sent to AI * @param {string} model - Model used * @returns {Object} Event attributes */ genAI: { prompt: (messages, model) => ({ 'gen_ai.content.prompt': JSON.stringify(messages), 'gen_ai.request.model': model, 'gen_ai.provider.name': getProviderFromModel(model) }), /** * GenAI completion event * @param {Object} response - AI response * @returns {Object} Event attributes */ completion: (response) => ({ 'gen_ai.content.completion': response.content, 'gen_ai.response.model': response.model, 'gen_ai.usage.prompt_tokens': response.usage?.prompt_tokens || 0, 'gen_ai.usage.completion_tokens': response.usage?.completion_tokens || 0 }) } }, // Metrics builders for dual emission (span attributes + queryable metrics) metrics: { /** * Emit a gauge metric (point-in-time value) * @param {string} name - Metric name (should match span attribute name) * @param {number} value - Metric value * @param {Object} attributes - Metric attributes (tags) */ gauge: (name, value, attributes = {}) => { try { const meter = getMeter(); const gauge = meter.createGauge(name, { description: `Gauge metric: ${name}`, unit: name.includes('_ms') || name.includes('duration') ? 'ms' : '1' }); const defaultAttributes = { 'service.name': 'commit-story-dev', 'environment': 'development' }; gauge.record(value, { ...defaultAttributes, ...attributes }); } catch (error) { console.warn(`Failed to emit gauge metric ${name}:`, error.message); } }, /** * Emit a counter metric (incrementing value) * @param {string} name - Metric name * @param {number} value - Increment value (default 1) * @param {Object} attributes - Metric attributes (tags) */ counter: (name, value = 1, attributes = {}) => { try { const meter = getMeter(); const counter = meter.createCounter(name, { description: `Counter metric: ${name}`, unit: '1' }); const defaultAttributes = { 'service.name': 'commit-story-dev', 'environment': 'development' }; counter.add(value, { ...defaultAttributes, ...attributes }); } catch (error) { console.warn(`Failed to emit counter metric ${name}:`, error.message); } }, /** * Emit a histogram metric (distribution data) * @param {string} name - Metric name * @param {number} value - Measurement value * @param {Object} attributes - Metric attributes (tags) */ histogram: (name, value, attributes = {}) => { try { const meter = getMeter(); const histogram = meter.createHistogram(name, { description: `Histogram metric: ${name}`, unit: name.includes('_ms') || name.includes('duration') ? 'ms' : '1' }); const defaultAttributes = { 'service.name': 'commit-story-dev', 'environment': 'development' }; histogram.record(value, { ...defaultAttributes, ...attributes }); } catch (error) { console.warn(`Failed to emit histogram metric ${name}:`, error.message); } } } }; /** * Utility for structured logging with trace context * @param {import('@opentelemetry/api').Span} span - Active span * @returns {Object} Trace context for logging */ export function getTraceContext(span) { if (!span) return {}; const spanContext = span.spanContext(); if (!spanContext) return {}; return { trace_id: spanContext.traceId, span_id: spanContext.spanId, service: OTEL.NAMESPACE }; }