Module: Smith::Trace

Defined in:
lib/smith/trace.rb,
lib/smith/trace/logger.rb,
lib/smith/trace/memory.rb,
lib/smith/trace/open_telemetry.rb

Defined Under Namespace

Classes: Logger, Memory, OpenTelemetry

Constant Summary collapse

SENSITIVITY_CONTENT_KEYS =
%i[args result].freeze

Class Method Summary collapse

Class Method Details

.apply_content_policy(data, sensitivity) ⇒ Object



53
54
55
56
57
58
59
60
61
62
# File 'lib/smith/trace.rb', line 53

def self.apply_content_policy(data, sensitivity)
  case Smith.config.trace_content
  when true
    apply_sensitivity(data, sensitivity)
  when :redacted
    apply_sensitivity(redact_sensitive_keys(data), sensitivity)
  else
    data.except(*SENSITIVITY_CONTENT_KEYS)
  end
end

.apply_sensitivity(data, sensitivity) ⇒ Object



64
65
66
67
68
69
70
71
72
73
# File 'lib/smith/trace.rb', line 64

def self.apply_sensitivity(data, sensitivity)
  case sensitivity
  when :high
    data.except(*SENSITIVITY_CONTENT_KEYS)
  when :medium
    redact_sensitive_keys(data)
  else
    data
  end
end

.attributed(data) ⇒ Object

Ambient attribution keys are identifiers, not content: they merge in under the caller's own keys (the caller wins on conflict) and then pass through the same content policy and field allowlist as everything else, so a host's configured trace_fields contract keeps holding.



26
27
28
29
30
31
32
33
# File 'lib/smith/trace.rb', line 26

def self.attributed(data)
  return data unless Smith.config.trace_attribution

  fields = Attribution.current_fields
  return data if fields.empty?

  fields.merge(data)
end

.filter_fields(type, data) ⇒ Object



96
97
98
99
100
101
102
103
104
105
106
# File 'lib/smith/trace.rb', line 96

def self.filter_fields(type, data)
  configured_fields = Smith.config.trace_fields
  return data unless configured_fields.is_a?(Hash)

  allowed = configured_fields[type]
  return data unless allowed.respond_to?(:include?)

  data.each_with_object({}) do |(key, value), filtered|
    filtered[key] = value if allowed.include?(key)
  end
end

.record(type:, data:, sensitivity: :low) ⇒ Object



11
12
13
14
15
16
17
18
19
20
# File 'lib/smith/trace.rb', line 11

def self.record(type:, data:, sensitivity: :low)
  adapter = resolve_adapter
  return unless adapter

  filtered = apply_content_policy(attributed(data), sensitivity)
  filtered = filter_fields(type, filtered)
  adapter.record(type: type, data: filtered)
rescue StandardError => e
  Smith.config.logger&.error("Smith::Trace adapter error: #{e.message}")
end

.redact_sensitive_keys(data) ⇒ Object



75
76
77
78
79
80
81
82
83
# File 'lib/smith/trace.rb', line 75

def self.redact_sensitive_keys(data)
  data.each_with_object({}) do |(key, value), filtered|
    filtered[key] = if SENSITIVITY_CONTENT_KEYS.include?(key)
                      redact_value(value)
                    else
                      value
                    end
  end
end

.redact_value(value) ⇒ Object



85
86
87
88
89
90
91
92
93
94
# File 'lib/smith/trace.rb', line 85

def self.redact_value(value)
  case value
  when String
    "[REDACTED]"
  when Hash
    value.transform_values { |v| v.is_a?(String) ? "[REDACTED]" : v }
  else
    value
  end
end

.reset! ⇒ Object



49
50
51
# File 'lib/smith/trace.rb', line 49

def self.reset!
  ADAPTER_MUTEX.synchronize { @adapter_instances = nil }
end

.resolve_adapter ⇒ Object

Class-configured adapters memoize one instance under a mutex so concurrent first records (every fan-out branch emits) share a single adapter instead of racing separate instances and losing entries.



38
39
40
41
42
43
44
45
46
47
# File 'lib/smith/trace.rb', line 38

def self.resolve_adapter
  configured = Smith.config.trace_adapter
  return nil unless configured
  return configured unless configured.is_a?(Class)

  ADAPTER_MUTEX.synchronize do
    @adapter_instances ||= {}
    @adapter_instances[configured] ||= configured.new
  end
end