UNPKG

enhanced-adot-node-autoinstrumentation

Version:

This package provides Amazon Web Services distribution of the OpenTelemetry Node Instrumentation, which allows for auto-instrumentation of NodeJS applications.

766 lines (763 loc) 35.2 kB
"use strict"; // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 // Modifications Copyright The OpenTelemetry Authors. Licensed under the Apache License 2.0 License. Object.defineProperty(exports, "__esModule", { value: true }); exports.createContextKey = exports.LLOHandler = void 0; const api_1 = require("@opentelemetry/api"); const sdk_events_1 = require("@opentelemetry/sdk-events"); // Message event types const GEN_AI_SYSTEM_MESSAGE = 'gen_ai.system.message'; const GEN_AI_USER_MESSAGE = 'gen_ai.user.message'; const GEN_AI_ASSISTANT_MESSAGE = 'gen_ai.assistant.message'; // Framework-specific attribute keys const TRACELOOP_ENTITY_INPUT = 'traceloop.entity.input'; const TRACELOOP_ENTITY_OUTPUT = 'traceloop.entity.output'; const TRACELOOP_CREW_TASKS_OUTPUT = 'crewai.crew.tasks_output'; const TRACELOOP_CREW_RESULT = 'crewai.crew.result'; const OPENINFERENCE_INPUT_VALUE = 'input.value'; const OPENINFERENCE_OUTPUT_VALUE = 'output.value'; const OPENLIT_PROMPT = 'gen_ai.prompt'; const OPENLIT_COMPLETION = 'gen_ai.completion'; const OPENLIT_REVISED_PROMPT = 'gen_ai.content.revised_prompt'; const OPENLIT_AGENT_ACTUAL_OUTPUT = 'gen_ai.agent.actual_output'; const OPENLIT_AGENT_HUMAN_INPUT = 'gen_ai.agent.human_input'; // Patterns for attribute filtering - using a set for O(1) lookups const exactMatchPatterns = new Set([ TRACELOOP_ENTITY_INPUT, TRACELOOP_ENTITY_OUTPUT, TRACELOOP_CREW_TASKS_OUTPUT, TRACELOOP_CREW_RESULT, OPENLIT_PROMPT, OPENLIT_COMPLETION, OPENLIT_REVISED_PROMPT, OPENLIT_AGENT_ACTUAL_OUTPUT, OPENLIT_AGENT_HUMAN_INPUT, OPENINFERENCE_INPUT_VALUE, OPENINFERENCE_OUTPUT_VALUE, ]); // Roles const ROLE_SYSTEM = 'system'; const ROLE_USER = 'user'; const ROLE_ASSISTANT = 'assistant'; // Patterns used in extraction methods const promptContentPattern = new RegExp('^gen_ai\\.prompt\\.(\\d+)\\.content$'); const completionContentPattern = new RegExp('^gen_ai\\.completion\\.(\\d+)\\.content$'); const openinferenceInputMsgPattern = new RegExp('^llm\\.input_messages\\.(\\d+)\\.message\\.content$'); const openinferenceOutputMsgPattern = new RegExp('^llm\\.output_messages\\.(\\d+)\\.message\\.content$'); const regexPatterns = [ promptContentPattern, completionContentPattern, openinferenceInputMsgPattern, openinferenceOutputMsgPattern, ]; /** * Utility class for handling Large Language Objects (LLO) in OpenTelemetry spans. * * LLOHandler performs three primary functions: * 1. Identifies Large Language Objects (LLO) content in spans * 2. Extracts and transforms these attributes into OpenTelemetry Gen AI Events * 3. Filters LLO from spans to maintain privacy and reduce span size * * Supported frameworks and their attribute patterns: * - Standard Gen AI: * - gen_ai.prompt.{n}.content: Structured prompt content * - gen_ai.prompt.{n}.role: Role for prompt content (system, user, assistant, etc.) * - gen_ai.completion.{n}.content: Structured completion content * - gen_ai.completion.{n}.role: Role for completion content (usually assistant) * * - Traceloop: * - traceloop.entity.input: Input text for LLM operations * - traceloop.entity.output: Output text from LLM operations * - traceloop.entity.name: Name of the entity processing the LLO * - crewai.crew.tasks_output: Tasks output data from CrewAI (uses gen_ai.system if available) * - crewai.crew.result: Final result from CrewAI crew (uses gen_ai.system if available) * * - OpenLit: * - gen_ai.prompt: Direct prompt text (treated as user message) * - gen_ai.completion: Direct completion text (treated as assistant message) * - gen_ai.content.revised_prompt: Revised prompt text (treated as system message) * - gen_ai.agent.actual_output: Output from CrewAI agent (treated as assistant message) * * - OpenInference: * - input.value: Direct input prompt * - output.value: Direct output response * - llm.input_messages.{n}.message.content: Individual structured input messages * - llm.input_messages.{n}.message.role: Role for input messages * - llm.output_messages.{n}.message.content: Individual structured output messages * - llm.output_messages.{n}.message.role: Role for output messages * - llm.model_name: Model name used for the LLM operation */ class LLOHandler { /** * Initialize an LLOHandler with the specified logger provider. * * This constructor sets up the event logger provider, configures the event logger, * and initializes the patterns used to identify LLO attributes. * * @param loggerProvider The OpenTelemetry LoggerProvider used for emitting events. * Global LoggerProvider instance injected from our AwsOpenTelemetryConfigurator */ constructor(loggerProvider) { this.loggerProvider = loggerProvider; this.eventLoggerProvider = new sdk_events_1.EventLoggerProvider(this.loggerProvider); this.eventLogger = this.eventLoggerProvider.getEventLogger('gen_ai.events'); } /** * Processes a sequence of spans to extract and filter LLO attributes. * * For each span, this method: * 1. Extracts LLO attributes and emits them as Gen AI Events * 2. Filters out LLO attributes from the span to maintain privacy * 3. Processes any LLO attributes in span events * 4. Preserves non-LLO attributes in the span * * Handles LLO attributes from multiple frameworks: * - Standard Gen AI (structured prompt/completion pattern) * - Traceloop (entity input/output pattern) * - OpenLit (direct prompt/completion pattern) * - OpenInference (input/output value and structured messages pattern) * * @param spans An array of OpenTelemetry ReadableSpan objects to process * @returns {ReadableSpan[]} Modified spans with LLO attributes removed */ processSpans(spans) { const modifiedSpans = []; for (const span of spans) { this.emitLloAttributes(span, span.attributes); const updatedAttributes = this.filterAttributes(span.attributes); const mutableSpan = span; mutableSpan.attributes = updatedAttributes; this.processSpanEvents(span); modifiedSpans.push(span); } return modifiedSpans; } /** * Process events within a span to extract and filter LLO attributes. * * For each event in the span, this method: * 1. Emits LLO attributes found in event attributes as Gen AI Events * 2. Filters out LLO attributes from event attributes * 3. Creates updated events with filtered attributes * 4. Replaces the original span events with updated events * * This ensures that LLO attributes are properly handled even when they appear * in span events rather than directly in the span's attributes. * * @param span The ReadableSpan to process events for */ processSpanEvents(span) { if (!span.events) { return; } const updatedEvents = []; for (const event of span.events) { if (!event.attributes) { updatedEvents.push(event); continue; } this.emitLloAttributes(span, event.attributes, event.time); const updatedEventAttributes = this.filterAttributes(event.attributes); if (Object.keys(updatedEventAttributes).length !== Object.keys(event.attributes).length) { const updatedEvent = { time: event.time, name: event.name, }; if (event.droppedAttributesCount) { updatedEvent.droppedAttributesCount = event.droppedAttributesCount; } if (updatedEventAttributes) { updatedEvent.attributes = updatedEventAttributes; } updatedEvents.push(updatedEvent); } else { updatedEvents.push(event); } } const mutableSpan = span; mutableSpan.events = updatedEvents; } /** * Extract Gen AI Events from LLO attributes and emit them via the event logger. * * This method: * 1. Collects LLO attributes from multiple frameworks using specialized extractors * 2. Converts each LLO attribute into appropriate Gen AI Events * 3. Emits all collected events through the event logger * * Supported frameworks: * - Standard Gen AI: Structured prompt/completion with roles * - Traceloop: Entity input/output and CrewAI outputs * - OpenLit: Direct prompt/completion/revised prompt and agent outputs * - OpenInference: Direct values and structured messages * * @param span The source ReadableSpan containing the attributes * @param attributes Attributes to process * @param eventTimestamp Optional timestamp to override span timestamps */ emitLloAttributes(span, attributes, eventTimestamp = undefined) { // Quick check if we have any LLO attributes before running extractors let hasLloAttrs = false; for (const key in attributes) { if (this.isLloAttribute(key)) { hasLloAttrs = true; break; } } if (!hasLloAttrs) { return; } const allEvents = [ ...this.extractGenAiPromptEvents(span, attributes, eventTimestamp), ...this.extractGenAiCompletionEvents(span, attributes, eventTimestamp), ...this.extractTraceloopEvents(span, attributes, eventTimestamp), ...this.extractOpenlitSpanEventAttributes(span, attributes, eventTimestamp), ...this.extractOpeninferenceAttributes(span, attributes, eventTimestamp), ]; for (const event of allEvents) { this.eventLogger.emit(event); api_1.diag.debug(`Emitted Gen AI Event: ${event.name}`); } } /** * Create a new attributes dictionary with LLO attributes removed. * This method creates a new dictionary containing only non-LLO attributes, * preserving the original values while filtering out sensitive LLO content. * This helps maintain privacy and reduces the size of spans. * * @param attributes Span or event attributes * @returns {Attributes} New Attributes with LLO attributes removed */ filterAttributes(attributes) { // First check if we need to filter anything let hasLloAttrs = false; for (const key in attributes) { if (this.isLloAttribute(key)) { hasLloAttrs = true; break; } } // If no LLO attributes found, return the original attributes (no need to copy) if (!hasLloAttrs) { return attributes; } // Otherwise, create filtered copy const filteredAttributes = {}; for (const [key, value] of Object.entries(attributes)) { if (!this.isLloAttribute(key)) { filteredAttributes[key] = value; } } return filteredAttributes; } /** * Determine if an attribute key contains LLO content based on pattern matching. * * Checks attribute keys against two types of patterns: * 1. Exact match patterns (complete string equality): * - Traceloop: "traceloop.entity.input", "traceloop.entity.output" * - OpenLit: "gen_ai.prompt", "gen_ai.completion", "gen_ai.content.revised_prompt" * - OpenInference: "input.value", "output.value" * * 2. Regex match patterns (regular expression matching): * - Standard Gen AI: "gen_ai.prompt.{n}.content", "gen_ai.completion.{n}.content" * - OpenInference: "llm.input_messages.{n}.message.content", "llm.output_messages.{n}.message.content" * * @param key The attribute key to check * @returns {boolean} true if the key matches any LLO pattern, false otherwise */ isLloAttribute(key) { // Check exact matches first (O(1) lookup in a set) if (exactMatchPatterns.has(key)) { return true; } // Then check regex patterns for (const pattern of regexPatterns) { if (key.match(pattern)) { return true; } } return false; } /** * Extract Gen AI Events from structured prompt attributes. * * Processes attributes matching the pattern `gen_ai.prompt.{n}.content` and their * associated `gen_ai.prompt.{n}.role` attributes to create appropriate events. * * Event types are determined by the role: * 1. `system` → `gen_ai.system.message` Event * 2. `user` → `gen_ai.user.message` Event * 3. `assistant` → `gen_ai.assistant.message` Event * 4. `function` → `gen_ai.{gen_ai.system}.message` custom Event * 5. `unknown` → `gen_ai.{gen_ai.system}.message` custom Event * * @param span The source ReadableSpan containing the attributes * @param attributes Attributes to process * @param eventTimestamp Optional timestamp to override span.startTime * @returns {Event[]} Events created from prompt attributes */ extractGenAiPromptEvents(span, attributes, eventTimestamp = undefined) { // Quick check if any prompt content attributes exist let promptContentPatternMatched = false; for (const key in attributes) { if (key.match(promptContentPattern)) { promptContentPatternMatched = true; break; } } if (!promptContentPatternMatched) { return []; } const events = []; const spanCtx = span.spanContext(); const genAiSystem = span.attributes['gen_ai.system'] || 'unknown'; // Use helper method to get appropriate timestamp (prompts are inputs) const promptTimestamp = this.getTimestamp(span, eventTimestamp, true); // Find all prompt content attributes and their roles const promptContentMatches = []; for (const [key, value] of Object.entries(attributes)) { const match = key.match(promptContentPattern); if (match) { const index = match[1]; const roleKey = `gen_ai.prompt.${index}.role`; const role = attributes[roleKey] || 'unknown'; promptContentMatches.push({ key, value, role }); } } // Create events for each content+role pair for (const { key, value, role } of promptContentMatches) { const eventAttributes = { 'gen_ai.system': genAiSystem, original_attribute: key }; const body = { content: value, role: role }; // Use helper method to determine event name based on role const eventName = this.getEventNameForRole(role, genAiSystem); const event = this.getGenAiEvent(eventName, spanCtx, promptTimestamp, eventAttributes, body, span); events.push(event); } return events; } /** * Extract Gen AI Events from structured completion attributes. * * Processes attributes matching the pattern `gen_ai.completion.{n}.content` and their * associated `gen_ai.completion.{n}.role` attributes to create appropriate events. * * Event types are determined by the role: * 1. `assistant` → `gen_ai.assistant.message` Event (most common) * 2. Other roles → `gen_ai.{gen_ai.system}.message` custom Event * * @param span The source ReadableSpan containing the attributes * @param attributes Attributes to process * @param eventTimestamp Optional timestamp to override span.endTime * @returns {Event[]} Events created from completion attributes */ extractGenAiCompletionEvents(span, attributes, eventTimestamp = undefined) { // Quick check if any completion content attributes exist let completionContentPatternMatched = false; for (const key in attributes) { if (key.match(completionContentPattern)) { completionContentPatternMatched = true; break; } } if (!completionContentPatternMatched) { return []; } const events = []; const spanCtx = span.spanContext(); const genAiSystem = span.attributes['gen_ai.system'] || 'unknown'; // Use helper method to get appropriate timestamp (completions are outputs) const completionTimestamp = this.getTimestamp(span, eventTimestamp, false); // Find all completion content attributes and their roles const completionContentMatches = []; for (const [key, value] of Object.entries(attributes)) { const match = key.match(completionContentPattern); if (match) { const index = match[1]; const roleKey = `gen_ai.completion.${index}.role`; const role = attributes[roleKey] || 'unknown'; completionContentMatches.push({ key, value, role }); } } // Create events for each content+role pair for (const { key, value, role } of completionContentMatches) { const eventAttributes = { 'gen_ai.system': genAiSystem, original_attribute: key }; const body = { content: value, role: role }; // Use helper method to determine event name based on role const eventName = this.getEventNameForRole(role, genAiSystem); const event = this.getGenAiEvent(eventName, spanCtx, completionTimestamp, eventAttributes, body, span); events.push(event); } return events; } /** * Extract Gen AI Events from Traceloop attributes. * * Processes Traceloop-specific attributes: * - `traceloop.entity.input`: Input data (uses span.startTime) * - `traceloop.entity.output`: Output data (uses span.endTime) * - `traceloop.entity.name`: Used as the gen_ai.system value when gen_ai.system isn't available * - `crewai.crew.tasksOutput`: Tasks output data from CrewAI (uses span.endTime) * - `crewai.crew.result`: Final result from CrewAI crew (uses span.endTime) * * Creates generic `gen_ai.{entity_name}.message` events for both input and output, * and assistant message events for CrewAI outputs. * * For CrewAI-specific attributes (crewai.crew.tasks_output and crewai.crew.result), * uses span's gen_ai.system attribute if available, otherwise falls back to traceloop.entity.name. * * @param span The source ReadableSpan containing the attributes * @param attributes Attributes to process * @param eventTimestamp Optional timestamp to override span timestamps * @returns {Event[]} Events created from Traceloop attributes */ extractTraceloopEvents(span, attributes, eventTimestamp = undefined) { // Define the Traceloop attributes we're looking for const traceloopKeys = [ TRACELOOP_ENTITY_INPUT, TRACELOOP_ENTITY_OUTPUT, TRACELOOP_CREW_TASKS_OUTPUT, TRACELOOP_CREW_RESULT, ]; // Quick check if any Traceloop attributes exist let traceloopAttributesExist = false; for (const key of traceloopKeys) { if (key in attributes) { traceloopAttributesExist = true; break; } } if (!traceloopAttributesExist) { return []; } const events = []; const spanCtx = span.spanContext(); // Use traceloop.entity.name for the gen_ai.system value const genAiSystem = span.attributes['traceloop.entity.name'] || 'unknown'; // Use helper methods to get appropriate timestamps const inputTimestamp = this.getTimestamp(span, eventTimestamp, true); const outputTimestamp = this.getTimestamp(span, eventTimestamp, false); // Standard Traceloop entity attributes const traceloopAttrs = [ { attrKey: TRACELOOP_ENTITY_INPUT, timestamp: inputTimestamp, role: ROLE_USER }, { attrKey: TRACELOOP_ENTITY_OUTPUT, timestamp: outputTimestamp, role: ROLE_ASSISTANT }, // Treat output as assistant role ]; for (const traceloopAttr of traceloopAttrs) { const { attrKey, timestamp, role } = traceloopAttr; if (attrKey in attributes) { const eventAttributes = { 'gen_ai.system': genAiSystem, original_attribute: attrKey }; const body = { content: attributes[attrKey], role: role }; // Custom event name for Traceloop (always use system-specific format) const eventName = `gen_ai.${genAiSystem}.message`; const event = this.getGenAiEvent(eventName, spanCtx, timestamp, eventAttributes, body, span); events.push(event); } } // CrewAI-specific Traceloop attributes // For CrewAI attributes, prefer gen_ai.system if available, otherwise use traceloop.entity.name const crewaiGenAiSystem = span.attributes['gen_ai.system'] || genAiSystem; const crewaiAttrs = [ { attrKey: TRACELOOP_CREW_TASKS_OUTPUT, timestamp: outputTimestamp, role: ROLE_ASSISTANT }, { attrKey: TRACELOOP_CREW_RESULT, timestamp: outputTimestamp, role: ROLE_ASSISTANT }, ]; for (const crewaiAttr of crewaiAttrs) { const { attrKey, timestamp, role } = crewaiAttr; if (attrKey in attributes) { const eventAttributes = { 'gen_ai.system': crewaiGenAiSystem, original_attribute: attrKey }; const body = { content: attributes[attrKey], role: role }; // For CrewAI outputs, use the assistant message event const eventName = GEN_AI_ASSISTANT_MESSAGE; const event = this.getGenAiEvent(eventName, spanCtx, timestamp, eventAttributes, body, span); events.push(event); } } return events; } /** * Extract Gen AI Events from OpenLit direct attributes. * OpenLit uses direct key-value pairs for LLO attributes: * - `gen_ai.prompt`: Direct prompt text (treated as user message) * - `gen_ai.completion`: Direct completion text (treated as assistant message) * - `gen_ai.content.revised_prompt`: Revised prompt text (treated as system message) * - `gen_ai.agent.actual_output`: Output from CrewAI agent (treated as assistant message) * The event timestamps are set based on attribute type: * - Prompt and revised prompt: span.startTime * - Completion and agent output: span.endTime * * @param span The source ReadableSpan containing the attributes * @param attributes Attributes to process * @param eventTimestamp Optional timestamp to override span timestamps * @returns {Event[]} Events created from OpenLit attributes */ extractOpenlitSpanEventAttributes(span, attributes, eventTimestamp = undefined) { // Define the OpenLit attributes we're looking for const openlitKeys = [ OPENLIT_PROMPT, OPENLIT_COMPLETION, OPENLIT_REVISED_PROMPT, OPENLIT_AGENT_ACTUAL_OUTPUT, OPENLIT_AGENT_HUMAN_INPUT, ]; // Quick check if any OpenLit attributes exist let openlitAttributesExist = false; for (const key of openlitKeys) { if (key in attributes) { openlitAttributesExist = true; break; } } if (!openlitAttributesExist) { return []; } const events = []; const spanCtx = span.spanContext(); const genAiSystem = span.attributes['gen_ai.system'] || 'unknown'; // Use helper methods to get appropriate timestamps const promptTimestamp = this.getTimestamp(span, eventTimestamp, true); const completionTimestamp = this.getTimestamp(span, eventTimestamp, false); const openlitEventAttrs = [ { attrKey: OPENLIT_PROMPT, timestamp: promptTimestamp, role: ROLE_USER, }, { attrKey: OPENLIT_COMPLETION, timestamp: completionTimestamp, role: ROLE_ASSISTANT, }, { attrKey: OPENLIT_REVISED_PROMPT, timestamp: promptTimestamp, role: ROLE_SYSTEM, }, { attrKey: OPENLIT_AGENT_ACTUAL_OUTPUT, timestamp: completionTimestamp, role: ROLE_ASSISTANT, }, { attrKey: OPENLIT_AGENT_HUMAN_INPUT, timestamp: promptTimestamp, role: ROLE_USER, }, // Assume user role for agent human input ]; for (const openlitEventAttr of openlitEventAttrs) { const { attrKey, timestamp, role } = openlitEventAttr; if (attrKey in attributes) { const eventAttributes = { 'gen_ai.system': genAiSystem, original_attribute: attrKey }; const body = { content: attributes[attrKey], role: role }; // Use helper method to determine event name based on role const eventName = this.getEventNameForRole(role, genAiSystem); const event = this.getGenAiEvent(eventName, spanCtx, timestamp, eventAttributes, body, span); events.push(event); } } return events; } /** * Extract Gen AI Events from OpenInference attributes. * * OpenInference uses two patterns for LLO attributes: * 1. Direct values: * - `input.value`: Direct input prompt (treated as user message) * - `output.value`: Direct output response (treated as assistant message) * * 2. Structured messages: * - `llm.input_messages.{n}.message.content`: Individual input messages * - `llm.input_messages.{n}.message.role`: Role for input message * - `llm.output_messages.{n}.message.content`: Individual output messages * - `llm.output_messages.{n}.message.role`: Role for output message * * The LLM model name is extracted from the `llm.model_name` attribute * instead of `gen_ai.system` which other frameworks use. * * Event timestamps are set based on message type: * - Input messages: span.startTime * - Output messages: span.endTime * * @param span The source ReadableSpan containing the attributes * @param attributes Attributes to process * @param eventTimestamp Optional timestamp to override span timestamps * @returns {Event[]} Events created from OpenInference attributes */ extractOpeninferenceAttributes(span, attributes, eventTimestamp = undefined) { // Define the OpenInference keys/patterns we're looking for const openinferenceDirectKeys = [OPENINFERENCE_INPUT_VALUE, OPENINFERENCE_OUTPUT_VALUE]; // Quick check if any OpenInference attributes exist let hasDirectAttrs = false; for (const key of openinferenceDirectKeys) { if (key in attributes) { hasDirectAttrs = true; break; } } let hasInputMsgs = false; for (const key in attributes) { if (key.match(openinferenceInputMsgPattern)) { hasInputMsgs = true; break; } } let hasOutputMsgs = false; for (const key in attributes) { if (key.match(openinferenceOutputMsgPattern)) { hasOutputMsgs = true; break; } } if (!(hasDirectAttrs || hasInputMsgs || hasOutputMsgs)) { return []; } const events = []; const spanCtx = span.spanContext(); const genAiSystem = span.attributes['llm.model_name'] || 'unknown'; // Use helper methods to get appropriate timestamps const inputTimestamp = this.getTimestamp(span, eventTimestamp, true); const outputTimestamp = this.getTimestamp(span, eventTimestamp, false); // Process direct value attributes const openinferenceDirectAttrs = [ { attrKey: OPENINFERENCE_INPUT_VALUE, timestamp: inputTimestamp, role: ROLE_USER }, { attrKey: OPENINFERENCE_OUTPUT_VALUE, timestamp: outputTimestamp, role: ROLE_ASSISTANT }, ]; for (const openinferenceDirectAttr of openinferenceDirectAttrs) { const { attrKey, timestamp, role } = openinferenceDirectAttr; if (attrKey in attributes) { const eventAttributes = { 'gen_ai.system': genAiSystem, original_attribute: attrKey }; const body = { content: attributes[attrKey], role: role }; // Use helper method to determine event name based on role const eventName = this.getEventNameForRole(role, genAiSystem); const event = this.getGenAiEvent(eventName, spanCtx, timestamp, eventAttributes, body, span); events.push(event); } } // Process input messages const inputMessages = []; for (const [key, value] of Object.entries(attributes)) { const match = key.match(openinferenceInputMsgPattern); if (match) { const index = match[1]; const roleKey = `llm.input_messages.${index}.message.role`; const role = attributes[roleKey] || ROLE_USER; // Default to user if role not specified inputMessages.push({ key, value, role }); } } // Create events for input messages for (const { key, value, role } of inputMessages) { const eventAttributes = { 'gen_ai.system': genAiSystem, original_attribute: key }; const body = { content: value, role: role }; // Use helper method to determine event name based on role const eventName = this.getEventNameForRole(role, genAiSystem); const event = this.getGenAiEvent(eventName, spanCtx, inputTimestamp, eventAttributes, body, span); events.push(event); } // Process output messages const outputMessages = []; for (const [key, value] of Object.entries(attributes)) { const match = key.match(openinferenceOutputMsgPattern); if (match) { const index = match[1]; const roleKey = `llm.output_messages.${index}.message.role`; const role = attributes[roleKey] || ROLE_ASSISTANT; // Default to assistant if role not specified outputMessages.push({ key, value, role }); } } // Create events for output messages for (const { key, value, role } of outputMessages) { const eventAttributes = { 'gen_ai.system': genAiSystem, original_attribute: key }; const body = { content: value, role: role }; // Use helper method to determine event name based on role const eventName = this.getEventNameForRole(role, genAiSystem); const event = this.getGenAiEvent(eventName, spanCtx, outputTimestamp, eventAttributes, body, span); events.push(event); } return events; } /** * Map a message role to the appropriate event name. * * @param role The role of the message (system, user, assistant, etc.) * @param genAiSystem The gen_ai system identifier * @returns {string} The appropriate event name for the given role */ getEventNameForRole(role, genAiSystem) { if (role === ROLE_SYSTEM) { return GEN_AI_SYSTEM_MESSAGE; } else if (role === ROLE_USER) { return GEN_AI_USER_MESSAGE; } else if (role === ROLE_ASSISTANT) { return GEN_AI_ASSISTANT_MESSAGE; } else { return `gen_ai.${genAiSystem}.message`; } } /** * Determine the appropriate timestamp to use for an event. * * @param span The source span * @param eventTimestamp Optional override timestamp * @param isInput Whether this is an input (true) or output (false) message * @returns {number} The timestamp to use for the event */ getTimestamp(span, eventTimestamp, isInput) { if (eventTimestamp !== undefined) { return eventTimestamp; } if (isInput) { return span.startTime; } else { return span.endTime; } } /** * Create and return a Gen AI Event with the specified parameters. * * This helper method constructs a fully configured OpenTelemetry Event object * that includes all necessary fields for proper event propagation and context. * * @param name Event type name (e.g., gen_ai.system.message, gen_ai.user.message) * @param spanCtx Span context to extract trace/span IDs from * @param timestamp Timestamp for the event (nanoseconds) * @param attributes Additional attributes to include with the event * @param data Event body containing content and role information * @param span A ReadableSpan associated with the Span context * @returns {Event}: A fully configured OpenTelemetry Gen AI Event object with proper trace context propagation */ getGenAiEvent(name, spanCtx, timestamp, attributes, data, span) { //[] Workaround to add a Context to an Event. // This is needed because a ReadableSpan only provides its SpanContext, // but does not provide access to the associated Context. An Event can // have a Context, but not a SpanContext. Here we attempt to attach a // custom Context that is associated to the ReadableSpan to mimic the // ReadableSpan's actual Context. const customContext = api_1.ROOT_CONTEXT.setValue(SPAN_KEY, span); return { name: name, timestamp: timestamp, attributes: attributes, data: data, context: customContext, }; } } exports.LLOHandler = LLOHandler; // The OpenTelemetry Authors code const SPAN_KEY = createContextKey('OpenTelemetry Context Key SPAN'); function createContextKey(description) { // The specification states that for the same input, multiple calls should // return different keys. Due to the nature of the JS dependency management // system, this creates problems where multiple versions of some package // could hold different keys for the same property. // // Therefore, we use Symbol.for which returns the same key for the same input. return Symbol.for(description); } exports.createContextKey = createContextKey; // END The OpenTelemetry Authors code //# sourceMappingURL=llo-handler.js.map