diff --git a/semantic_conventions_ai/lib/opentelemetry/semantic_conventions.rb b/semantic_conventions_ai/lib/opentelemetry/semantic_conventions.rb index 891eaf9..b7b55fc 100644 --- a/semantic_conventions_ai/lib/opentelemetry/semantic_conventions.rb +++ b/semantic_conventions_ai/lib/opentelemetry/semantic_conventions.rb @@ -30,6 +30,17 @@ module SpanAttributes # Deprecated TRACELOOP_CORRELATION_ID = "traceloop.correlation.id" + + # Gen AI + GEN_AI_REQUEST_MODEL = "gen_ai.request.model" + GEN_AI_RESPONSE_MODEL = "gen_ai.response.model" + GEN_AI_USAGE_OUTPUT_TOKENS = "gen_ai.usage.output_tokens" + GEN_AI_USAGE_INPUT_TOKENS = "gen_ai.usage.input_tokens" + GEN_AI_COMPLETIONS = "gen_ai.completion" + GEN_AI_PROMPTS = "gen_ai.prompt" + GEN_AI_SYSTEM = "gen_ai.system" + GEN_AI_PROVIDER = "gen_ai.provider.name" + GEN_AI_CONVERSATION_ID = "gen_ai.conversation.id" end module LLMRequestTypeValues diff --git a/traceloop-sdk/lib/traceloop/sdk.rb b/traceloop-sdk/lib/traceloop/sdk.rb index 640be53..e5dd2d3 100644 --- a/traceloop-sdk/lib/traceloop/sdk.rb +++ b/traceloop-sdk/lib/traceloop/sdk.rb @@ -6,16 +6,21 @@ module Traceloop module SDK class Traceloop def initialize + api_key = ENV["TRACELOOP_API_KEY"] + raise "TRACELOOP_API_KEY environment variable is required" if api_key.nil? || api_key.empty? + OpenTelemetry::SDK.configure do |c| c.add_span_processor( - OpenTelemetry::SDK::Trace::Export::SimpleSpanProcessor.new( + OpenTelemetry::SDK::Trace::Export::BatchSpanProcessor.new( OpenTelemetry::Exporter::OTLP::Exporter.new( endpoint: "#{ENV.fetch("TRACELOOP_BASE_URL", "https://api.traceloop.com")}/v1/traces", - headers: { "Authorization" => "Bearer #{ENV.fetch("TRACELOOP_API_KEY")}" } + headers: { + "Authorization" => "#{ENV.fetch("TRACELOOP_AUTH_SCHEME", "Bearer")} #{ENV.fetch("TRACELOOP_API_KEY")}" + } ) ) ) - puts "Traceloop exporting traces to #{ENV.fetch("TRACELOOP_BASE", "https://api.traceloop.com")}" + puts "Traceloop exporting traces to #{ENV.fetch("TRACELOOP_BASE_URL", "https://api.traceloop.com")}" end @tracer = OpenTelemetry.tracer_provider.tracer("Traceloop") @@ -41,15 +46,15 @@ def log_messages(messages) def log_prompt(system_prompt="", user_prompt) unless system_prompt.empty? @span.add_attributes({ - "#{OpenTelemetry::SemanticConventionsAi::SpanAttributes::LLM_PROMPTS}.0.role" => "system", - "#{OpenTelemetry::SemanticConventionsAi::SpanAttributes::LLM_PROMPTS}.0.content" => system_prompt, - "#{OpenTelemetry::SemanticConventionsAi::SpanAttributes::LLM_PROMPTS}.1.role" => "user", - "#{OpenTelemetry::SemanticConventionsAi::SpanAttributes::LLM_PROMPTS}.1.content" => user_prompt + "#{OpenTelemetry::SemanticConventionsAi::SpanAttributes::GEN_AI_PROMPTS}.0.role" => "system", + "#{OpenTelemetry::SemanticConventionsAi::SpanAttributes::GEN_AI_PROMPTS}.0.content" => system_prompt, + "#{OpenTelemetry::SemanticConventionsAi::SpanAttributes::GEN_AI_PROMPTS}.1.role" => "user", + "#{OpenTelemetry::SemanticConventionsAi::SpanAttributes::GEN_AI_PROMPTS}.1.content" => user_prompt }) else @span.add_attributes({ - "#{OpenTelemetry::SemanticConventionsAi::SpanAttributes::LLM_PROMPTS}.0.role" => "user", - "#{OpenTelemetry::SemanticConventionsAi::SpanAttributes::LLM_PROMPTS}.0.content" => user_prompt + "#{OpenTelemetry::SemanticConventionsAi::SpanAttributes::GEN_AI_PROMPTS}.0.role" => "user", + "#{OpenTelemetry::SemanticConventionsAi::SpanAttributes::GEN_AI_PROMPTS}.0.content" => user_prompt }) end end @@ -57,9 +62,14 @@ def log_prompt(system_prompt="", user_prompt) def log_response(response) if response.respond_to?(:body) log_bedrock_response(response) + # Check for RubyLLM::Message objects + elsif defined?(::RubyLLM::Message) && response.is_a?(::RubyLLM::Message) + log_ruby_llm_message(response) + elsif defined?(::RubyLLM::Tool::Halt) && response.is_a?(::RubyLLM::Tool::Halt) + log_ruby_llm_halt(response) # This is Gemini specific, see - # https://github.com/gbaptista/gemini-ai?tab=readme-ov-file#generate_content - elsif response.has_key?("candidates") + elsif response.respond_to?(:has_key?) && response.has_key?("candidates") log_gemini_response(response) else log_openai_response(response) @@ -73,10 +83,29 @@ def log_gemini_response(response) @span.add_attributes({ "#{OpenTelemetry::SemanticConventionsAi::SpanAttributes::LLM_COMPLETIONS}.0.role" => "assistant", - "#{OpenTelemetry::SemanticConventionsAi::SpanAttributes::LLM_COMPLETIONS}.0.content" => response.dig("candidates", 0, "content", "parts", 0, "text") + "#{OpenTelemetry::SemanticConventionsAi::SpanAttributes::LLM_COMPLETIONS}.0.content" => response.dig( +"candidates", 0, "content", "parts", 0, "text") }) end + def log_ruby_llm_message(response) + @span.add_attributes({ + OpenTelemetry::SemanticConventionsAi::SpanAttributes::GEN_AI_RESPONSE_MODEL => response.model_id, + OpenTelemetry::SemanticConventionsAi::SpanAttributes::GEN_AI_USAGE_OUTPUT_TOKENS => response.output_tokens || 0, + OpenTelemetry::SemanticConventionsAi::SpanAttributes::GEN_AI_USAGE_INPUT_TOKENS => response.input_tokens || 0, + "#{OpenTelemetry::SemanticConventionsAi::SpanAttributes::GEN_AI_COMPLETIONS}.0.role" => response.role.to_s, + "#{OpenTelemetry::SemanticConventionsAi::SpanAttributes::GEN_AI_COMPLETIONS}.0.content" => response.content + }) + end + + def log_ruby_llm_halt(response) + @span.add_attributes({ + OpenTelemetry::SemanticConventionsAi::SpanAttributes::GEN_AI_RESPONSE_MODEL => @model, + "#{OpenTelemetry::SemanticConventionsAi::SpanAttributes::GEN_AI_COMPLETIONS}.0.role" => "tool", + "#{OpenTelemetry::SemanticConventionsAi::SpanAttributes::GEN_AI_COMPLETIONS}.0.content" => response.content + }) + end + def log_bedrock_response(response) body = JSON.parse(response.body.read()) @@ -109,25 +138,38 @@ def log_openai_response(response) }) if response.has_key?("usage") @span.add_attributes({ - OpenTelemetry::SemanticConventionsAi::SpanAttributes::LLM_USAGE_TOTAL_TOKENS => response.dig("usage", "total_tokens"), - OpenTelemetry::SemanticConventionsAi::SpanAttributes::LLM_USAGE_COMPLETION_TOKENS => response.dig("usage", "completion_tokens"), - OpenTelemetry::SemanticConventionsAi::SpanAttributes::LLM_USAGE_PROMPT_TOKENS => response.dig("usage", "prompt_tokens"), + OpenTelemetry::SemanticConventionsAi::SpanAttributes::LLM_USAGE_TOTAL_TOKENS => response.dig("usage", + "total_tokens"), + OpenTelemetry::SemanticConventionsAi::SpanAttributes::LLM_USAGE_COMPLETION_TOKENS => response.dig( +"usage", "completion_tokens"), + OpenTelemetry::SemanticConventionsAi::SpanAttributes::LLM_USAGE_PROMPT_TOKENS => response.dig("usage", + "prompt_tokens"), }) end if response.has_key?("choices") @span.add_attributes({ - "#{OpenTelemetry::SemanticConventionsAi::SpanAttributes::LLM_COMPLETIONS}.0.role" => response.dig("choices", 0, "message", "role"), - "#{OpenTelemetry::SemanticConventionsAi::SpanAttributes::LLM_COMPLETIONS}.0.content" => response.dig("choices", 0, "message", "content") + "#{OpenTelemetry::SemanticConventionsAi::SpanAttributes::LLM_COMPLETIONS}.0.role" => response.dig( +"choices", 0, "message", "role"), + "#{OpenTelemetry::SemanticConventionsAi::SpanAttributes::LLM_COMPLETIONS}.0.content" => response.dig( +"choices", 0, "message", "content") }) end end end - def llm_call(provider, model) + def llm_call(provider, model, conversation_id: nil) @tracer.in_span("#{provider}.chat") do |span| - span.add_attributes({ - OpenTelemetry::SemanticConventionsAi::SpanAttributes::LLM_REQUEST_MODEL => model, - }) + attributes = { + OpenTelemetry::SemanticConventionsAi::SpanAttributes::GEN_AI_REQUEST_MODEL => model, + OpenTelemetry::SemanticConventionsAi::SpanAttributes::GEN_AI_SYSTEM => provider, + OpenTelemetry::SemanticConventionsAi::SpanAttributes::GEN_AI_PROVIDER => provider, + } + + if conversation_id + attributes[OpenTelemetry::SemanticConventionsAi::SpanAttributes::GEN_AI_CONVERSATION_ID] = conversation_id + end + + span.add_attributes(attributes) yield Tracer.new(span, provider, model) end end diff --git a/traceloop-sdk/traceloop-sdk.gemspec b/traceloop-sdk/traceloop-sdk.gemspec index b985c60..9acbe62 100644 --- a/traceloop-sdk/traceloop-sdk.gemspec +++ b/traceloop-sdk/traceloop-sdk.gemspec @@ -17,8 +17,8 @@ Gem::Specification.new do |spec| spec.add_dependency 'opentelemetry-semantic_conventions_ai', '~> 0.0.3' - spec.add_dependency 'opentelemetry-sdk', '~> 1.3.1' - spec.add_dependency 'opentelemetry-exporter-otlp', '~> 0.26.1' + spec.add_dependency 'opentelemetry-exporter-otlp', '~> 0.31.1' + spec.add_dependency 'opentelemetry-sdk', '~> 1.10.0' if spec.respond_to?(:metadata) spec.metadata['source_code_uri'] = 'https://github.com/traceloop/openllmetry-ruby/tree/main/traceloop-sdk'