Module: Ast::Merge::DebugLogger

Extended by:
DebugLogger
Included in:
DebugLogger
Defined in:
lib/ast/merge/debug_logger.rb

Overview

Note:

Shared examples require silent_stream and rspec-stubbed_env gems.

Base debug logging utility for AST merge libraries. Provides conditional debug output based on environment configuration.

This module is designed to be extended by file-type-specific merge libraries (e.g., Prism::Merge, Psych::Merge) which configure their own environment variable and log prefix.

Minimal Integration

Simply extend this module and configure your environment variable and log prefix:

Overriding Methods

When you extend a module, its instance methods become singleton methods on your module. To override inherited behavior, you must define singleton methods (+def self.method_name+), not instance methods (+def method_name+).

Testing with Shared Examples

Use the provided shared examples to validate your integration:

require "ast/merge/rspec/shared_examples"

RSpec.describe MyMerge::DebugLogger do
it_behaves_like "Ast::Merge::DebugLogger" do
  let(:described_logger) { MyMerge::DebugLogger }
  let(:env_var_name) { "MY_MERGE_DEBUG" }
  let(:log_prefix) { "[MyMerge]" }
end
end

Examples:

Creating a custom debug logger (minimal integration)

module MyMerge
  module DebugLogger
    extend Ast::Merge::DebugLogger

    self.env_var_name = "MY_MERGE_DEBUG"
    self.log_prefix = "[MyMerge]"
  end
end

Overriding a method (correct - singleton method)

module MyMerge
  module DebugLogger
    extend Ast::Merge::DebugLogger

    self.env_var_name = "MY_MERGE_DEBUG"
    self.log_prefix = "[MyMerge]"

    # Override extract_node_info for custom node types
    def self.extract_node_info(node)
      case node
      when MyMerge::CustomNode
        {type: "CustomNode", lines: "#{node.start_line}..#{node.end_line}"}
      else
        # Delegate to base implementation
        Ast::Merge::DebugLogger.extract_node_info(node)
      end
    end
  end
end

Enable debug logging

ENV['AST_MERGE_DEBUG'] = '1'
Ast::Merge::DebugLogger.debug("Processing node", {type: "mapping", line: 5})

Constant Summary collapse

BENCHMARK_AVAILABLE =

Benchmark is optional - gracefully degrade if not available. As of Ruby 4.0, benchmark is a bundled gem (not default), so it may not be available. We attempt to require it at load time and set a flag for later use.

begin
  require 'benchmark'
  true
rescue LoadError
  # benchmark gem not available (Ruby 4.0+ without explicit dependency, or unusual Ruby builds)
  false
end
UNIVERSAL_DEBUG_ENV_VAR =
'KETTLE_DEV_DEBUG'

Class Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Class Attribute Details

.env_var_nameString

Returns Environment variable name to check for debug mode # - Configuration attribute, set once at load time.

Returns:

  • (String)

    Environment variable name to check for debug mode # - Configuration attribute, set once at load time



88
89
90
# File 'lib/ast/merge/debug_logger.rb', line 88

def env_var_name
  @env_var_name
end

.log_prefixString

Returns Prefix for log messages # - Configuration attribute, set once at load time.

Returns:

  • (String)

    Prefix for log messages # - Configuration attribute, set once at load time



90
91
92
# File 'lib/ast/merge/debug_logger.rb', line 90

def log_prefix
  @log_prefix
end

Class Method Details

.extended(base) ⇒ Object

Hook called when a module extends Ast::Merge::DebugLogger. Sets up attr_accessor for env_var_name and log_prefix on the extending module, and copies the BENCHMARK_AVAILABLE constant.

Parameters:

  • base (Module)

    The module that is extending this module



97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
# File 'lib/ast/merge/debug_logger.rb', line 97

def extended(base)
  # Create a module with the accessors and prepend it to the singleton class.
  # This avoids "method redefined" warnings when extending multiple times.
  accessors_module = Module.new do
    attr_accessor :env_var_name
    attr_accessor :log_prefix
  end
  base.singleton_class.prepend(accessors_module)

  # Set default values (inherit from Ast::Merge::DebugLogger)
  base.env_var_name = env_var_name
  base.log_prefix = log_prefix

  # Copy the BENCHMARK_AVAILABLE constant
  base.const_set(:BENCHMARK_AVAILABLE, BENCHMARK_AVAILABLE) unless base.const_defined?(:BENCHMARK_AVAILABLE)
end

Instance Method Details

#debug(message, context = {}) ⇒ Object

Log a debug message with optional context

Parameters:

  • message (String)

    The debug message

  • context (Hash) (defaults to: {})

    Optional context to include



164
165
166
167
168
169
170
# File 'lib/ast/merge/debug_logger.rb', line 164

def debug(message, context = {})
  return unless enabled?

  output = "#{log_prefix} #{message}"
  output += " #{context.inspect}" unless context.empty?
  warn(output)
end

#debug_warning(message, context = {}) ⇒ Object

Log a warning message only when debug mode is enabled.

Use this for internal safety rails that indicate a logic bug when they fire, but should remain silent in normal runs.

Parameters:

  • message (String)

    The warning message

  • context (Hash) (defaults to: {})

    Optional context to include



195
196
197
198
199
200
201
# File 'lib/ast/merge/debug_logger.rb', line 195

def debug_warning(message, context = {})
  return unless enabled?

  output = "#{log_prefix} WARNING] #{message}"
  output += " #{context.inspect}" unless context.empty?
  warn(output)
end

#enabled?Boolean

Check if debug mode is enabled

Returns:

  • (Boolean)


124
125
126
# File 'lib/ast/merge/debug_logger.rb', line 124

def enabled?
  truthy_env_value?(ENV[UNIVERSAL_DEBUG_ENV_VAR]) || truthy_env_value?(ENV[env_var_name])
end

#env_var_nameString

Get the environment variable name. When called as a module method (via extend self), returns own config. When called as instance method, checks class first, then falls back to base.

Returns:

  • (String)


133
134
135
136
137
138
139
140
141
142
# File 'lib/ast/merge/debug_logger.rb', line 133

def env_var_name
  if is_a?(Module) && singleton_class.method_defined?(:env_var_name)
    # Called as module method on a module that extended us
    self.class.superclass == Module ? @env_var_name : self.class.env_var_name
  elsif self.class.respond_to?(:env_var_name)
    self.class.env_var_name
  else
    Ast::Merge::DebugLogger.env_var_name
  end
end

#extract_lines(node) ⇒ String?

Extract line information from a node if available

Parameters:

  • node (Object)

    Node to extract lines from

Returns:

  • (String, nil)

    Line range string or nil



271
272
273
274
275
276
277
278
279
280
281
282
# File 'lib/ast/merge/debug_logger.rb', line 271

def extract_lines(node)
  if node.respond_to?(:location)
    loc = node.location
    if loc.respond_to?(:start_line) && loc.respond_to?(:end_line)
      "#{loc.start_line}..#{loc.end_line}"
    elsif loc.respond_to?(:start_line)
      loc.start_line.to_s
    end
  elsif node.respond_to?(:start_line) && node.respond_to?(:end_line)
    "#{node.start_line}..#{node.end_line}"
  end
end

#extract_node_info(node) ⇒ Hash

Extract information from a node for logging. Override in submodules for file-type-specific node types.

Parameters:

  • node (Object)

    Node to extract info from

Returns:

  • (Hash)

    Node information



243
244
245
246
247
248
249
250
# File 'lib/ast/merge/debug_logger.rb', line 243

def extract_node_info(node)
  type_name = safe_type_name(node)
  lines = extract_lines(node)

  info = { type: type_name }
  info[:lines] = lines if lines
  info
end

#info(message) ⇒ Object

Log an info message (always shown when debug is enabled)

Parameters:

  • message (String)

    The info message



175
176
177
178
179
# File 'lib/ast/merge/debug_logger.rb', line 175

def info(message)
  return unless enabled?

  warn("#{log_prefix} INFO] #{message}")
end

#log_node(node, label: 'Node') ⇒ Object

Log node information - override in submodules for file-type-specific logging

Parameters:

  • node (Object)

    Node to log information about

  • label (String) (defaults to: 'Node')

    Label for the node



231
232
233
234
235
236
# File 'lib/ast/merge/debug_logger.rb', line 231

def log_node(node, label: 'Node')
  return unless enabled?

  info = extract_node_info(node)
  debug(label, info)
end

#log_prefixString

Get the log prefix. When called as a module method (via extend self), returns own config. When called as instance method, checks class first, then falls back to base.

Returns:

  • (String)


149
150
151
152
153
154
155
156
157
158
# File 'lib/ast/merge/debug_logger.rb', line 149

def log_prefix
  if is_a?(Module) && singleton_class.method_defined?(:log_prefix)
    # Called as module method on a module that extended us
    self.class.superclass == Module ? @log_prefix : self.class.log_prefix
  elsif self.class.respond_to?(:log_prefix)
    self.class.log_prefix
  else
    Ast::Merge::DebugLogger.log_prefix
  end
end

#safe_type_name(node) ⇒ String

Safely extract the type name from a node

Parameters:

  • node (Object)

    Node to get type from

Returns:

  • (String)

    Type name



256
257
258
259
260
261
262
263
264
265
# File 'lib/ast/merge/debug_logger.rb', line 256

def safe_type_name(node)
  klass = node.class
  if klass.respond_to?(:name) && klass.name
    klass.name.split('::').last
  else
    klass.to_s
  end
rescue StandardError
  node.class.to_s
end

#time(operation) { ... } ⇒ Object

Time a block and log the duration

Parameters:

  • operation (String)

    Name of the operation

Yields:

  • The block to time

Returns:

  • (Object)

    The result of the block



208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
# File 'lib/ast/merge/debug_logger.rb', line 208

def time(operation)
  return yield unless enabled?

  unless BENCHMARK_AVAILABLE
    warning("Benchmark gem not available - timing disabled for: #{operation}")
    return yield
  end

  debug("Starting: #{operation}")
  result = nil
  timing = Benchmark.measure { result = yield }
  debug("Completed: #{operation}", {
          real_ms: (timing.real * 1000).round(2),
          user_ms: (timing.utime * 1000).round(2),
          system_ms: (timing.stime * 1000).round(2)
        })
  result
end

#truthy_env_value?(value) ⇒ Boolean

Returns:

  • (Boolean)


284
285
286
# File 'lib/ast/merge/debug_logger.rb', line 284

def truthy_env_value?(value)
  %w[1 true].include?(value.to_s.downcase)
end

#warning(message) ⇒ Object

Log a warning message (always shown)

Parameters:

  • message (String)

    The warning message



184
185
186
# File 'lib/ast/merge/debug_logger.rb', line 184

def warning(message)
  warn("#{log_prefix} WARNING] #{message}")
end