Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
143 changes: 30 additions & 113 deletions lib/observable/instrumenter.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
require "opentelemetry/sdk"
require_relative "configuration"
require_relative "serialization_depth"

module Observable
class Instrumenter
Expand All @@ -8,6 +9,7 @@ class Instrumenter
def initialize(tracer: nil, config: nil)
@config = config || Configuration.config
@tracer = tracer || OpenTelemetry.tracer_provider.tracer(@config.tracer_name)
@serialization_depth = SerializationDepth.new(@config.serialization_depth)
end

def instrument(caller_binding = nil, &block)
Expand Down Expand Up @@ -52,7 +54,7 @@ def extract_namespace_from_path(file_path)
return "UnknownClass" if file_name.empty?

file_name.split("_").map(&:capitalize).join
rescue
rescue StandardError
"UnknownClass"
end

Expand All @@ -65,14 +67,14 @@ def extract_actual_class_name
else
caller_self.class.name
end
rescue
rescue StandardError
extract_namespace_from_path(@caller_binding.eval("__FILE__"))
end
else
caller_location = find_caller_location
extract_namespace_from_path(caller_location.path)
end
rescue
rescue StandardError
"UnknownClass"
end

Expand All @@ -82,28 +84,20 @@ def determine_if_class_method
begin
caller_self = @caller_binding.eval("self")
caller_self.is_a?(Class)
rescue
rescue StandardError
false
end
end

def create_instrumented_span(caller_info, &block)
separator = caller_info.is_class_method ? "." : "#"

if caller_info.method_name.include?("#") || caller_info.method_name.include?(".")
span_name = "#{caller_info.namespace}#{separator}#{caller_info.method_name.split(/[#.]/).last}"
function_name = "#{caller_info.namespace}#{separator}#{caller_info.method_name.split(/[#.]/).last}"
method_name_only = caller_info.method_name.split(/[#.]/).last
else
span_name = "#{caller_info.namespace}#{separator}#{caller_info.method_name}"
function_name = "#{caller_info.namespace}#{separator}#{caller_info.method_name}"
method_name_only = caller_info.method_name
end
method_name_only = caller_info.method_name.split(/[#.]/).last
qualified_name = "#{caller_info.namespace}#{separator}#{method_name_only}"

@last_captured_method_name = method_name_only
@last_captured_namespace = caller_info.namespace
@tracer.in_span(span_name) do |span|
set_span_attributes(span, caller_info, function_name)
@tracer.in_span(qualified_name) do |span|
set_span_attributes(span, caller_info, qualified_name)

begin
result = block.call
Expand Down Expand Up @@ -143,7 +137,7 @@ def set_span_attributes(span, caller_info, function_name)
end
end

def serialize_argument(span, attribute_prefix, value, param_name = nil, depth = 0)
def serialize_argument(span, attribute_prefix, value, param_name = nil, depth = 0, max_depth = nil)
if param_name && should_filter_pii?(param_name)
span.set_attribute(attribute_prefix, "[FILTERED]")
return
Expand All @@ -155,29 +149,11 @@ def serialize_argument(span, attribute_prefix, value, param_name = nil, depth =
when NilClass
span.set_attribute(attribute_prefix, "nil")
when Hash
serialize_hash(span, attribute_prefix, value, depth)
serialize_hash(span, attribute_prefix, value, depth, max_depth)
when Array
serialize_array(span, attribute_prefix, value, depth)
serialize_array(span, attribute_prefix, value, depth, max_depth)
else
serialize_object(span, attribute_prefix, value, depth)
end
end

def serialize_argument_with_custom_depth(span, attribute_prefix, value, custom_max_depth, param_name = nil, depth = 0)
if param_name && should_filter_pii?(param_name)
span.set_attribute(attribute_prefix, "[FILTERED]")
return
end

case value
when String, Numeric, TrueClass, FalseClass, NilClass
span.set_attribute(attribute_prefix, value)
when Hash
serialize_hash_with_custom_depth(span, attribute_prefix, value, custom_max_depth, depth)
when Array
serialize_array_with_custom_depth(span, attribute_prefix, value, custom_max_depth, depth)
else
serialize_object_with_custom_depth(span, attribute_prefix, value, custom_max_depth, depth)
serialize_object(span, attribute_prefix, value, depth, max_depth)
end
end

Expand All @@ -190,89 +166,38 @@ def serialize_return_value(span, value)
when String, Numeric, TrueClass, FalseClass
span.set_attribute("code.return", value)
when Hash
serialize_hash(span, "code.return", value, 2)
serialize_hash(span, "code.return", value, DEFAULT_SERIALIZATION_DEPTH)
when Array
serialize_array(span, "code.return", value, 2)
serialize_array(span, "code.return", value, DEFAULT_SERIALIZATION_DEPTH)
else
serialize_object(span, "code.return", value, 2)
serialize_object(span, "code.return", value, DEFAULT_SERIALIZATION_DEPTH)
end
end

def serialize_hash(span, prefix, hash, depth = 0)
max_depth = get_serialization_depth_for_class("Hash")
def serialize_hash(span, prefix, hash, depth = 0, max_depth = nil)
max_depth ||= get_serialization_depth_for_class("Hash")
return if depth > max_depth

hash.each do |key, value|
key_str = key.to_s
next if should_filter_pii?(key_str)
serialize_argument(span, "#{prefix}.#{key_str}", value, nil, depth + 1)
end
end

def serialize_hash_with_custom_depth(span, prefix, hash, custom_max_depth, depth = 0)
return if depth > custom_max_depth

hash.each do |key, value|
key_str = key.to_s
next if should_filter_pii?(key_str)
serialize_argument_with_custom_depth(span, "#{prefix}.#{key_str}", value, custom_max_depth, nil, depth + 1)
serialize_argument(span, "#{prefix}.#{key_str}", value, nil, depth + 1, max_depth)
end
end

def serialize_array_with_custom_depth(span, prefix, array, custom_max_depth, depth = 0)
return if depth > custom_max_depth

items_to_process = [array.length, 10].min
array.first(items_to_process).each_with_index do |item, index|
serialize_argument_with_custom_depth(span, "#{prefix}.#{index}", item, custom_max_depth, nil, depth + 1)
end
end

def serialize_object_with_custom_depth(span, prefix, obj, custom_max_depth, depth = 0)
if depth >= custom_max_depth
span.set_attribute("#{prefix}.class", obj.class.name)
span.set_attribute(prefix, obj.to_s)
return
end

span.set_attribute("#{prefix}.class", obj.class.name)

formatter = find_formatter_for_class(obj.class.name)

if formatter && obj.respond_to?(formatter)
result = obj.send(formatter)
if result.is_a?(Hash)
serialize_hash_with_custom_depth(span, prefix, result, custom_max_depth, depth)
return
end
end

default_formatter = @config.formatters[:default]
if default_formatter && obj.respond_to?(default_formatter)
result = obj.send(default_formatter)
if result.is_a?(Hash)
serialize_hash_with_custom_depth(span, prefix, result, custom_max_depth, depth)
return
end
end

span.set_attribute(prefix, obj.to_s)
end

def serialize_array(span, prefix, array, depth = 0)
max_depth = get_serialization_depth_for_class("Array")
def serialize_array(span, prefix, array, depth = 0, max_depth = nil)
max_depth ||= get_serialization_depth_for_class("Array")
return if depth > max_depth

items_to_process = [array.length, 10].min
items_to_process = [array.length, MAX_ARRAY_ITEMS].min
array.first(items_to_process).each_with_index do |item, index|
serialize_argument(span, "#{prefix}.#{index}", item, nil, depth + 1)
serialize_argument(span, "#{prefix}.#{index}", item, nil, depth + 1, max_depth)
end
end

def serialize_object(span, prefix, obj, depth = 0)
max_depth = get_serialization_depth_for_class(obj.class.name)
def serialize_object(span, prefix, obj, depth = 0, max_depth = nil)
max_depth ||= get_serialization_depth_for_class(obj.class.name)

# If we've reached the depth limit for this class, just set the class name
if depth >= max_depth
span.set_attribute("#{prefix}.class", obj.class.name)
span.set_attribute(prefix, obj.to_s)
Expand All @@ -286,9 +211,7 @@ def serialize_object(span, prefix, obj, depth = 0)
if formatter && obj.respond_to?(formatter)
result = obj.send(formatter)
if result.is_a?(Hash)
# When object converts to hash, the hash starts fresh at depth 0
# but we need to enforce the object's depth limit
serialize_hash_with_custom_depth(span, prefix, result, max_depth, 0)
serialize_hash(span, prefix, result, 0, max_depth)
return
end
end
Expand All @@ -297,9 +220,7 @@ def serialize_object(span, prefix, obj, depth = 0)
if default_formatter && obj.respond_to?(default_formatter)
result = obj.send(default_formatter)
if result.is_a?(Hash)
# When object converts to hash, the hash starts fresh at depth 0
# but we need to enforce the object's depth limit
serialize_hash_with_custom_depth(span, prefix, result, max_depth, 0)
serialize_hash(span, prefix, result, 0, max_depth)
return
end
end
Expand All @@ -312,11 +233,7 @@ def find_formatter_for_class(class_name)
end

def get_serialization_depth_for_class(class_name)
if @config.serialization_depth.is_a?(Integer)
return @config.serialization_depth
end

@config.serialization_depth[class_name] || @config.serialization_depth[:default] || @config.serialization_depth["default"] || 2
@serialization_depth.for_class(class_name)
end

def should_filter_pii?(param_name)
Expand Down Expand Up @@ -353,7 +270,7 @@ def extract_from_binding
extract_local_variables_with_numeric_indexing(args)

args
rescue
rescue StandardError
{}
end

Expand Down
7 changes: 1 addition & 6 deletions lib/observable/persistence/span_repo.rb
Original file line number Diff line number Diff line change
Expand Up @@ -130,12 +130,7 @@ def get_value(hash, key)
end

def colorize_trace_header(trace_id)
# Use the same color constants from Span
cyan = "\e[36m"
bold = "\e[1m"
reset = "\e[0m"

"#{cyan}#{bold}#{trace_id}#{reset}"
"#{Span::CYAN}#{Span::BOLD}#{trace_id}#{Span::RESET}"
end
end
end
Expand Down
19 changes: 19 additions & 0 deletions lib/observable/serialization_depth.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# frozen_string_literal: true

module Observable
class SerializationDepth
DEFAULT_DEPTH = 2

def initialize(config)
@config = config
end

def for_class(class_name)
if @config.is_a?(Integer)
@config
else
@config[class_name] || @config[:default] || @config["default"] || DEFAULT_DEPTH
end
end
end
end
8 changes: 4 additions & 4 deletions lib/observable/structured_error.rb
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ def self.safe_message(error)
else
""
end
rescue
rescue StandardError
""
end

Expand All @@ -63,7 +63,7 @@ def self.safe_type(error)
end

error.class.name
rescue
rescue StandardError
error.class.name
end

Expand All @@ -77,7 +77,7 @@ def self.safe_context(error)
else
{}
end
rescue
rescue StandardError
{}
end

Expand All @@ -92,7 +92,7 @@ def self.try_custom_converter(error)

result = begin
converter.call(error)
rescue
rescue StandardError
nil
end

Expand Down