@aws/aws-distro-opentelemetry-node-autoinstrumentation
Version:
This package provides Amazon Web Services distribution of the OpenTelemetry Node Instrumentation, which allows for auto-instrumentation of NodeJS applications.
503 lines • 18.9 kB
JavaScript
"use strict";
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
Object.defineProperty(exports, "__esModule", { value: true });
exports.OTEL_SPAN_KEY = exports.LLOHandler = exports.LLO_PATTERNS = exports.PatternType = void 0;
const api_1 = require("@opentelemetry/api");
const api_logs_1 = require("@opentelemetry/api-logs");
const ROLE_SYSTEM = 'system';
const ROLE_USER = 'user';
const ROLE_ASSISTANT = 'assistant';
const SESSION_ID = 'session.id';
// Types of LLO attribute patterns
var PatternType;
(function (PatternType) {
PatternType["INDEXED"] = "indexed";
PatternType["DIRECT"] = "direct";
})(PatternType = exports.PatternType || (exports.PatternType = {}));
exports.LLO_PATTERNS = {
'gen_ai.prompt.{index}.content': {
type: PatternType.INDEXED,
regex: '^gen_ai\\.prompt\\.(\\d+)\\.content$',
roleKey: 'gen_ai.prompt.{index}.role',
defaultRole: 'unknown',
source: 'prompt',
},
'gen_ai.completion.{index}.content': {
type: PatternType.INDEXED,
regex: '^gen_ai\\.completion\\.(\\d+)\\.content$',
roleKey: 'gen_ai.completion.{index}.role',
defaultRole: 'unknown',
source: 'completion',
},
'llm.input_messages.{index}.message.content': {
type: PatternType.INDEXED,
regex: '^llm\\.input_messages\\.(\\d+)\\.message\\.content$',
roleKey: 'llm.input_messages.{index}.message.role',
defaultRole: ROLE_USER,
source: 'input',
},
'llm.output_messages.{index}.message.content': {
type: PatternType.INDEXED,
regex: '^llm\\.output_messages\\.(\\d+)\\.message\\.content$',
roleKey: 'llm.output_messages.{index}.message.role',
defaultRole: ROLE_ASSISTANT,
source: 'output',
},
'traceloop.entity.input': {
type: PatternType.DIRECT,
role: ROLE_USER,
source: 'input',
},
'traceloop.entity.output': {
type: PatternType.DIRECT,
role: ROLE_ASSISTANT,
source: 'output',
},
'crewai.crew.tasks_output': {
type: PatternType.DIRECT,
role: ROLE_ASSISTANT,
source: 'output',
},
'crewai.crew.result': {
type: PatternType.DIRECT,
role: ROLE_ASSISTANT,
source: 'result',
},
'gen_ai.prompt': {
type: PatternType.DIRECT,
role: ROLE_USER,
source: 'prompt',
},
'gen_ai.completion': {
type: PatternType.DIRECT,
role: ROLE_ASSISTANT,
source: 'completion',
},
'gen_ai.content.revised_prompt': {
type: PatternType.DIRECT,
role: ROLE_SYSTEM,
source: 'prompt',
},
'gen_ai.agent.actual_output': {
type: PatternType.DIRECT,
role: ROLE_ASSISTANT,
source: 'output',
},
'gen_ai.agent.human_input': {
type: PatternType.DIRECT,
role: ROLE_USER,
source: 'input',
},
'input.value': {
type: PatternType.DIRECT,
role: ROLE_USER,
source: 'input',
},
'output.value': {
type: PatternType.DIRECT,
role: ROLE_ASSISTANT,
source: 'output',
},
system_prompt: {
type: PatternType.DIRECT,
role: ROLE_SYSTEM,
source: 'prompt',
},
'tool.result': {
type: PatternType.DIRECT,
role: ROLE_ASSISTANT,
source: 'output',
},
'llm.prompts': {
type: PatternType.DIRECT,
role: ROLE_USER,
source: 'prompt',
},
'gen_ai.input.messages': {
type: PatternType.DIRECT,
role: ROLE_USER,
source: 'input',
},
'gen_ai.output.messages': {
type: PatternType.DIRECT,
role: ROLE_ASSISTANT,
source: 'output',
},
'gen_ai.system_instructions': {
type: PatternType.DIRECT,
role: ROLE_SYSTEM,
source: 'prompt',
},
};
/**
* 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
*
* The handler uses a configuration-driven approach with a pattern registry that defines
* all supported LLO attribute patterns and their extraction rules. This makes it easy
* to add support for new frameworks without modifying the core logic.
*/
class LLOHandler {
/**
* Initialize an LLOHandler with the specified logger provider.
*
* This constructor compiles patterns from the pattern registry for efficient matching.
*
* @param loggerProvider The OpenTelemetry LoggerProvider used for emitting log records.
*/
constructor(loggerProvider) {
this.loggerProvider = loggerProvider;
this.exactMatchPatterns = new Set();
this.regexPatterns = [];
this.patternConfigs = {};
this.buildPatternMatchers();
}
/**
* Build efficient pattern matching structures from the pattern registry.
*
* Creates:
* - Set of exact match patterns for O(1) lookups
* - List of compiled regex patterns for indexed patterns
* - Mapping of patterns to their configurations
*/
buildPatternMatchers() {
for (const [patternKey, config] of Object.entries(exports.LLO_PATTERNS)) {
if (config.type === PatternType.DIRECT) {
this.exactMatchPatterns.add(patternKey);
this.patternConfigs[patternKey] = config;
}
else if (config.type === PatternType.INDEXED) {
if (config.regex) {
const compiledRegex = new RegExp(config.regex);
this.regexPatterns.push([compiledRegex, patternKey, config]);
}
}
}
}
/**
* Processes a sequence of spans to extract and filter LLO attributes.
*
* For each span, this method:
* 1. Collects all LLO attributes from span attributes and all span events
* 2. Emits a single consolidated Gen AI Event with all collected LLO content
* 3. Filters out LLO attributes from the span and its events to maintain privacy
* 4. Preserves non-LLO attributes in the span
*
* Handles LLO attributes from multiple frameworks:
* - Traceloop (indexed prompt/completion patterns and entity input/output)
* - OpenLit (direct prompt/completion patterns, including from span events)
* - OpenInference (input/output values and structured messages)
* - Strands SDK (system prompts and tool results)
* - CrewAI (tasks output and results)
*
* @param spans A list of OpenTelemetry ReadableSpan objects to process
* @returns {ReadableSpan[]} A list of modified spans with LLO attributes removed
*/
processSpans(spans) {
const modifiedSpans = [];
for (const span of spans) {
// Collect all LLO attributes from both span attributes and events
const allLloAttributes = this.collectLloAttributesFromSpan(span);
// Emit a single consolidated event if we found any LLO attributes
if (Object.keys(allLloAttributes).length > 0) {
this.emitLloAttributes(span, allLloAttributes);
}
// Filter and update span attributes
const filteredAttributes = this.filterAttributes(span.attributes);
span.attributes = filteredAttributes;
// Filter span events
this.filterSpanEvents(span);
modifiedSpans.push(span);
}
return modifiedSpans;
}
/**
* Collect all LLO attributes from a span's attributes and events.
*
* @param span The span to collect LLO attributes from
* @returns all LLO attributes found in the span
*/
collectLloAttributesFromSpan(span) {
const allLloAttributes = {};
// Collect from span attributes
if (span.attributes) {
for (const [key, value] of Object.entries(span.attributes)) {
if (this.isLloAttribute(key)) {
allLloAttributes[key] = value;
}
}
}
// Collect from span events
if (span.events) {
for (const event of span.events) {
if (event.attributes) {
for (const [key, value] of Object.entries(event.attributes)) {
if (this.isLloAttribute(key)) {
allLloAttributes[key] = value;
}
}
}
}
}
return allLloAttributes;
}
/**
* Filter LLO attributes from span events.
*
* This method removes LLO attributes from event attributes while preserving
* the event structure and non-LLO attributes.
*
* @param span The ReadableSpan to filter events for
*/
filterSpanEvents(span) {
if (!span.events) {
return;
}
const updatedEvents = [];
for (const event of span.events) {
if (!event.attributes) {
updatedEvents.push(event);
continue;
}
const updatedEventAttributes = this.filterAttributes(event.attributes);
if (Object.keys(updatedEventAttributes).length !== Object.keys(event.attributes).length) {
const updatedEvent = {
name: event.name,
time: event.time,
attributes: updatedEventAttributes,
};
updatedEvents.push(updatedEvent);
}
else {
updatedEvents.push(event);
}
}
span.events = updatedEvents;
}
/**
* Extract LLO attributes and emit them as a single consolidated Gen AI Event.
*
* This method:
* 1. Collects all LLO attributes using the pattern registry
* 2. Groups them into input and output messages
* 3. Emits one event per span containing all LLO content
*
* The event body format:
* {
* "input": {
* "messages": [
* {
* "role": "system",
* "content": "..."
* },
* {
* "role": "user",
* "content": "..."
* }
* ]
* },
* "output": {
* "messages": [
* {
* "role": "assistant",
* "content": "..."
* }
* ]
* }
* }
*
* @param span The source ReadableSpan containing the attributes
* @param attributes LLO attributes to process
* @param eventTimestamp Optional timestamp to override span timestamps
* @returns
*/
emitLloAttributes(span, attributes, eventTimestamp) {
if (!attributes || Object.keys(attributes).length === 0) {
return;
}
const allMessages = this.collectAllLloMessages(span, attributes);
if (allMessages.length === 0) {
return;
}
// Group messages into input/output categories
const groupedMessages = this.groupMessagesByType(allMessages);
// Build event body
const eventBody = {};
if (groupedMessages.input.length > 0) {
eventBody.input = { messages: groupedMessages.input };
}
if (groupedMessages.output.length > 0) {
eventBody.output = { messages: groupedMessages.output };
}
if (Object.keys(eventBody).length === 0) {
return;
}
// Create and emit the log record
// This replaces the deprecated @opentelemetry/sdk-events EventLogger pattern
const timestamp = eventTimestamp || span.endTime;
const logger = this.loggerProvider.getLogger(span.instrumentationScope.name);
// Workaround to add a custom-made Context to the log record so that it has the correct
// associated traceId, spanId, flag. This is needed because a ReadableSpan only provides
// its SpanContext, but does not provide access to its associated Context.
// When a log record instance is created, it will use this context to extract the SpanContext.
// - https://github.com/open-telemetry/opentelemetry-js/blob/main/experimental/packages/sdk-logs/src/LogRecord.ts
const customContext = api_1.ROOT_CONTEXT.setValue(exports.OTEL_SPAN_KEY, span);
// Build attributes with event.name (for backwards compatibility with EventLogger behavior)
// and session ID if present
const logAttributes = {
'event.name': span.instrumentationScope.name,
};
if (span.attributes[SESSION_ID]) {
logAttributes[SESSION_ID] = span.attributes[SESSION_ID];
}
// Use eventName field to set the event name (aligns with Python LLO handler's use of 'name')
// Also keep event.name in attributes for backwards compatibility with EventLogger
// See: https://github.com/aws-observability/aws-otel-python-instrumentation/blob/main/aws-opentelemetry-distro/src/amazon/opentelemetry/distro/llo_handler.py#L534
logger.emit({
eventName: span.instrumentationScope.name,
timestamp: timestamp,
body: eventBody,
context: customContext,
attributes: logAttributes,
severityNumber: api_logs_1.SeverityNumber.INFO,
});
}
/**
* Collect all LLO messages from attributes using the pattern registry.
*
* This is the main collection method that processes all patterns defined
* in the registry and extracts messages accordingly.
*
* @param span The source ReadableSpan containing the attributes
* @param attributes Attributes to process
* @returns {Message[]} LLO messages from attributes using the pattern registry
*/
collectAllLloMessages(span, attributes) {
const messages = [];
if (!attributes)
return messages;
for (const [attrKey, value] of Object.entries(attributes)) {
if (this.exactMatchPatterns.has(attrKey)) {
const config = this.patternConfigs[attrKey];
messages.push({
content: value,
role: config.role || 'unknown',
source: config.source || 'unknown',
});
}
}
messages.push(...this.collectIndexedMessages(attributes));
return messages;
}
/**
* Collect messages from indexed patterns (e.g., gen_ai.prompt.0.content).
* Handles patterns with numeric indices and their associated role attributes.
*
* @param attributes Attributes to process
* @returns {Message[]}
*/
collectIndexedMessages(attributes) {
const indexedMessages = [];
for (const [attrKey, value] of Object.entries(attributes)) {
for (const [regex, patternKey, config] of this.regexPatterns) {
const match = attrKey.match(regex);
if (match) {
const index = parseInt(match[1], 10);
let role = config.defaultRole || 'unknown';
if (config.roleKey) {
const roleKey = config.roleKey.replace('{index}', index.toString());
const roleValue = attributes[roleKey];
if (typeof roleValue === 'string') {
role = roleValue;
}
}
indexedMessages.push({
content: value,
role,
source: config.source,
pattern: patternKey,
index: index,
});
break;
}
}
}
return indexedMessages
.sort((a, b) => (a.pattern !== b.pattern ? a.pattern.localeCompare(b.pattern) : a.index - b.index))
.map(({ content, role, source }) => ({ content, role, source }));
}
groupMessagesByType(messages) {
const input = [];
const output = [];
for (const message of messages) {
const { role, content, source } = message;
const formattedMessage = { role, content };
if (role === ROLE_SYSTEM || role === ROLE_USER) {
input.push(formattedMessage);
}
else if (role === ROLE_ASSISTANT) {
output.push(formattedMessage);
}
else {
// Route based on source for non-standard roles
if (['completion', 'output', 'result'].some(key => source.includes(key))) {
output.push(formattedMessage);
}
else {
input.push(formattedMessage);
}
}
}
return { input, output };
}
/**
* Create new attributes with LLO attributes removed.
*
* This method creates an attributes object 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 Original span or event attributes
* @returns {Attributes} New attributes with LLO attributes removed, or empty object if input is undefined
*/
filterAttributes(attributes) {
if (!attributes) {
return {};
}
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.
* Uses the pattern registry to check if a key matches any LLO pattern.
*
* @param key The attribute key to check
* @returns {boolean} true if the key matches any LLO pattern, false otherwise
*/
isLloAttribute(key) {
if (this.exactMatchPatterns.has(key)) {
return true;
}
for (const [regex] of this.regexPatterns) {
if (regex.test(key)) {
return true;
}
}
return false;
}
}
exports.LLOHandler = LLOHandler;
// Defined by OTel in:
// - https://github.com/open-telemetry/opentelemetry-js/blob/v1.9.0/api/src/trace/context-utils.ts#L24-L27
exports.OTEL_SPAN_KEY = (0, api_1.createContextKey)('OpenTelemetry Context Key SPAN');
//# sourceMappingURL=llo-handler.js.map