From a93f31234034a20395a36e44010841eb64edaf34 Mon Sep 17 00:00:00 2001 From: John Gallagher Date: Mon, 9 Mar 2026 09:25:34 +0000 Subject: [PATCH 1/5] Simplify redundant conditional in create_instrumented_span (#8) The if/else block assigned the same expression to span_name, function_name, and method_name_only, calling split(/[#.]/).last twice in the true branch. Replaced with two clear variables: method_name_only and qualified_name. https://claude.ai/code/session_01KWZUse79XR68i6nGa8VgTu Co-authored-by: Claude --- lib/observable/instrumenter.rb | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/lib/observable/instrumenter.rb b/lib/observable/instrumenter.rb index 6ac0a75..e8719c0 100644 --- a/lib/observable/instrumenter.rb +++ b/lib/observable/instrumenter.rb @@ -89,21 +89,13 @@ def determine_if_class_method 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 From 98fab5a1d83826adf44a114cb920e5ec58ca8365 Mon Sep 17 00:00:00 2001 From: John Gallagher Date: Mon, 9 Mar 2026 10:08:42 +0000 Subject: [PATCH 2/5] Replace bare rescue clauses with explicit StandardError (#9) Bare rescue catches all StandardError subclasses implicitly, but also signals unclear intent. Being explicit with 'rescue StandardError' makes the intent obvious and passes linting checks. Fixes 10 occurrences across instrumenter.rb (5) and structured_error.rb (5). https://claude.ai/code/session_01KWZUse79XR68i6nGa8VgTu Co-authored-by: Claude --- lib/observable/instrumenter.rb | 10 +++++----- lib/observable/structured_error.rb | 8 ++++---- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/lib/observable/instrumenter.rb b/lib/observable/instrumenter.rb index e8719c0..982d0ad 100644 --- a/lib/observable/instrumenter.rb +++ b/lib/observable/instrumenter.rb @@ -52,7 +52,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 @@ -65,14 +65,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 @@ -82,7 +82,7 @@ def determine_if_class_method begin caller_self = @caller_binding.eval("self") caller_self.is_a?(Class) - rescue + rescue StandardError false end end @@ -345,7 +345,7 @@ def extract_from_binding extract_local_variables_with_numeric_indexing(args) args - rescue + rescue StandardError {} end diff --git a/lib/observable/structured_error.rb b/lib/observable/structured_error.rb index b3484da..247e5b7 100644 --- a/lib/observable/structured_error.rb +++ b/lib/observable/structured_error.rb @@ -45,7 +45,7 @@ def self.safe_message(error) else "" end - rescue + rescue StandardError "" end @@ -63,7 +63,7 @@ def self.safe_type(error) end error.class.name - rescue + rescue StandardError error.class.name end @@ -77,7 +77,7 @@ def self.safe_context(error) else {} end - rescue + rescue StandardError {} end @@ -92,7 +92,7 @@ def self.try_custom_converter(error) result = begin converter.call(error) - rescue + rescue StandardError nil end From b28bea12f1af7952685e418c84aef222611fc033 Mon Sep 17 00:00:00 2001 From: John Gallagher Date: Mon, 9 Mar 2026 10:09:12 +0000 Subject: [PATCH 3/5] Use Span ANSI constants in SpanRepo#colorize_trace_header (#10) The method was duplicating CYAN, BOLD, and RESET escape codes that are already defined as constants on Span. Replace the local variables with direct references to Span::CYAN, Span::BOLD, and Span::RESET. https://claude.ai/code/session_01KWZUse79XR68i6nGa8VgTu Co-authored-by: Claude --- lib/observable/persistence/span_repo.rb | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/lib/observable/persistence/span_repo.rb b/lib/observable/persistence/span_repo.rb index 2d1a7db..6b05be3 100644 --- a/lib/observable/persistence/span_repo.rb +++ b/lib/observable/persistence/span_repo.rb @@ -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 From bdcc2a971dfa5f9bd2693c37a0aeeb16221915cc Mon Sep 17 00:00:00 2001 From: John Gallagher Date: Mon, 9 Mar 2026 10:11:08 +0000 Subject: [PATCH 4/5] Consolidate duplicate _with_custom_depth serialization methods (#11) The four *_with_custom_depth methods (serialize_argument, serialize_hash, serialize_array, serialize_object) were near-identical copies of the base methods, differing only in whether max_depth was computed from config or passed explicitly. Unified into a single family using an optional max_depth parameter (nil = compute from config, value = use as-is and propagate down). Removes ~65 lines of duplicated code. https://claude.ai/code/session_01KWZUse79XR68i6nGa8VgTu Co-authored-by: Claude --- lib/observable/instrumenter.rb | 109 ++++++--------------------------- 1 file changed, 18 insertions(+), 91 deletions(-) diff --git a/lib/observable/instrumenter.rb b/lib/observable/instrumenter.rb index 982d0ad..93264b7 100644 --- a/lib/observable/instrumenter.rb +++ b/lib/observable/instrumenter.rb @@ -135,7 +135,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 @@ -147,29 +147,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 @@ -182,89 +164,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) - 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 + serialize_argument(span, "#{prefix}.#{key_str}", value, nil, depth + 1, max_depth) 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) @@ -278,9 +209,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 @@ -289,9 +218,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 From 99851c94fab963b071c47af0721c67c51c521099 Mon Sep 17 00:00:00 2001 From: John Gallagher Date: Mon, 9 Mar 2026 20:26:40 +0000 Subject: [PATCH 5/5] Introduce SerializationDepth value object to replace primitive obsession (#13) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit get_serialization_depth_for_class was doing type-checking (is Integer or Hash?), key-coercion (symbol vs string), and falling back to a hardcoded default — all inline. Extract that logic into Observable::SerializationDepth#for_class, making Instrumenter unaware of how depth config is shaped. https://claude.ai/code/session_01KWZUse79XR68i6nGa8VgTu Co-authored-by: Claude --- lib/observable/instrumenter.rb | 8 +++----- lib/observable/serialization_depth.rb | 19 +++++++++++++++++++ 2 files changed, 22 insertions(+), 5 deletions(-) create mode 100644 lib/observable/serialization_depth.rb diff --git a/lib/observable/instrumenter.rb b/lib/observable/instrumenter.rb index 93264b7..034753b 100644 --- a/lib/observable/instrumenter.rb +++ b/lib/observable/instrumenter.rb @@ -1,5 +1,6 @@ require "opentelemetry/sdk" require_relative "configuration" +require_relative "serialization_depth" module Observable class Instrumenter @@ -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) @@ -231,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) diff --git a/lib/observable/serialization_depth.rb b/lib/observable/serialization_depth.rb new file mode 100644 index 0000000..a7669a5 --- /dev/null +++ b/lib/observable/serialization_depth.rb @@ -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