Module: Tracekit::LLM::Common

Defined in:
lib/tracekit/llm/common.rb

Constant Summary collapse

SENSITIVE_KEY_PATTERN =

Pattern-based PII regexes (all replaced with plain [REDACTED])

/\A(password|passwd|pwd|secret|token|key|credential|api_key|apikey)\z/i
EMAIL_PATTERN =
/[a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,}/
SSN_PATTERN =
/\b\d{3}-\d{2}-\d{4}\b/
CREDIT_CARD_PATTERN =
/\b\d{4}[\s\-]?\d{4}[\s\-]?\d{4}[\s\-]?\d{4}\b/
AWS_KEY_PATTERN =
/\bAKIA[0-9A-Z]{16}\b/
BEARER_PATTERN =
/Bearer\s+[A-Za-z0-9\-._~+\/]+=*/
STRIPE_PATTERN =
/\bsk_live_[a-zA-Z0-9]+/
JWT_PATTERN =
/\beyJ[A-Za-z0-9\-_]+\.eyJ[A-Za-z0-9\-_]+\.[A-Za-z0-9\-_]+/
PRIVATE_KEY_PATTERN =
/-----BEGIN\s+(?:RSA\s+)?PRIVATE\s+KEY-----/
CONTENT_PATTERNS =
[
  EMAIL_PATTERN, SSN_PATTERN, CREDIT_CARD_PATTERN, AWS_KEY_PATTERN,
  BEARER_PATTERN, STRIPE_PATTERN, JWT_PATTERN, PRIVATE_KEY_PATTERN
].freeze

Class Method Summary collapse

Class Method Details

.capture_content?Boolean

Returns:

  • (Boolean)


63
64
65
66
67
# File 'lib/tracekit/llm/common.rb', line 63

def capture_content?
  env_val = ENV["TRACEKIT_LLM_CAPTURE_CONTENT"]
  return env_val.downcase == "true" || env_val == "1" if env_val
  false
end

.capture_input_messages(span, messages) ⇒ Object



99
100
101
102
103
# File 'lib/tracekit/llm/common.rb', line 99

def capture_input_messages(span, messages)
  return unless messages
  serialized = JSON.generate(messages)
  span.set_attribute("gen_ai.input.messages", scrub_pii(serialized))
end

.capture_output_messages(span, content) ⇒ Object



105
106
107
108
109
# File 'lib/tracekit/llm/common.rb', line 105

def capture_output_messages(span, content)
  return unless content
  serialized = JSON.generate(content)
  span.set_attribute("gen_ai.output.messages", scrub_pii(serialized))
end

.capture_system_instructions(span, system) ⇒ Object



111
112
113
114
115
# File 'lib/tracekit/llm/common.rb', line 111

def capture_system_instructions(span, system)
  return unless system
  serialized = system.is_a?(String) ? system : JSON.generate(system)
  span.set_attribute("gen_ai.system_instructions", scrub_pii(serialized))
end

.record_tool_call(span, name:, id: nil, arguments: nil) ⇒ Object



92
93
94
95
96
97
# File 'lib/tracekit/llm/common.rb', line 92

def record_tool_call(span, name:, id: nil, arguments: nil)
  attrs = { "gen_ai.tool.name" => name }
  attrs["gen_ai.tool.call.id"] = id if id
  attrs["gen_ai.tool.call.arguments"] = arguments if arguments
  span.add_event("gen_ai.tool.call", attributes: attrs)
end

.scrub_object(obj) ⇒ Object



44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
# File 'lib/tracekit/llm/common.rb', line 44

def scrub_object(obj)
  case obj
  when Hash
    obj.each_with_object({}) do |(k, v), h|
      if SENSITIVE_KEY_PATTERN.match?(k.to_s)
        h[k] = "[REDACTED]"
      else
        h[k] = scrub_object(v)
      end
    end
  when Array
    obj.map { |item| scrub_object(item) }
  when String
    scrub_patterns(obj)
  else
    obj
  end
end

.scrub_patterns(str) ⇒ Object



38
39
40
41
42
# File 'lib/tracekit/llm/common.rb', line 38

def scrub_patterns(str)
  result = str.dup
  CONTENT_PATTERNS.each { |pat| result.gsub!(pat, "[REDACTED]") }
  result
end

.scrub_pii(content) ⇒ Object



26
27
28
29
30
31
32
33
34
35
36
# File 'lib/tracekit/llm/common.rb', line 26

def scrub_pii(content)
  # Try JSON key-based scrubbing first
  begin
    parsed = JSON.parse(content)
    scrubbed = scrub_object(parsed)
    return JSON.generate(scrubbed)
  rescue JSON::ParserError
    # Not JSON, fall through to pattern scrubbing
  end
  scrub_patterns(content)
end

.set_error_attributes(span, error) ⇒ Object



86
87
88
89
90
# File 'lib/tracekit/llm/common.rb', line 86

def set_error_attributes(span, error)
  span.set_attribute("error.type", error.class.name)
  span.status = OpenTelemetry::Trace::Status.error(error.message)
  span.record_exception(error)
end

.set_request_attributes(span, provider:, model:, max_tokens: nil, temperature: nil, top_p: nil) ⇒ Object



69
70
71
72
73
74
75
76
# File 'lib/tracekit/llm/common.rb', line 69

def set_request_attributes(span, provider:, model:, max_tokens: nil, temperature: nil, top_p: nil)
  span.set_attribute("gen_ai.operation.name", "chat")
  span.set_attribute("gen_ai.system", provider)
  span.set_attribute("gen_ai.request.model", model)
  span.set_attribute("gen_ai.request.max_tokens", max_tokens) if max_tokens
  span.set_attribute("gen_ai.request.temperature", temperature) if temperature
  span.set_attribute("gen_ai.request.top_p", top_p) if top_p
end

.set_response_attributes(span, model: nil, id: nil, finish_reasons: nil, input_tokens: nil, output_tokens: nil) ⇒ Object



78
79
80
81
82
83
84
# File 'lib/tracekit/llm/common.rb', line 78

def set_response_attributes(span, model: nil, id: nil, finish_reasons: nil, input_tokens: nil, output_tokens: nil)
  span.set_attribute("gen_ai.response.model", model) if model
  span.set_attribute("gen_ai.response.id", id) if id
  span.set_attribute("gen_ai.response.finish_reasons", finish_reasons) if finish_reasons&.any?
  span.set_attribute("gen_ai.usage.input_tokens", input_tokens) if input_tokens
  span.set_attribute("gen_ai.usage.output_tokens", output_tokens) if output_tokens
end