Class: ActionMCP::LogSubscriber

Inherits:
ActiveSupport::LogSubscriber
  • Object
show all
Defined in:
lib/action_mcp/log_subscriber.rb

Class Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Class Attribute Details

.custom_metricsObject

Returns the value of attribute custom_metrics.



7
8
9
# File 'lib/action_mcp/log_subscriber.rb', line 7

def custom_metrics
  @custom_metrics
end

.formattersObject

Returns the value of attribute formatters.



7
8
9
# File 'lib/action_mcp/log_subscriber.rb', line 7

def formatters
  @formatters
end

.metric_groupsObject

Returns the value of attribute metric_groups.



7
8
9
# File 'lib/action_mcp/log_subscriber.rb', line 7

def metric_groups
  @metric_groups
end

.subscribed_eventsObject

Returns the value of attribute subscribed_events.



7
8
9
# File 'lib/action_mcp/log_subscriber.rb', line 7

def subscribed_events
  @subscribed_events
end

Class Method Details

.add_metric(name, value) ⇒ Object

Add a custom metric to be included in logs



35
36
37
38
# File 'lib/action_mcp/log_subscriber.rb', line 35

def self.add_metric(name, value)
  self.custom_metrics ||= {}
  self.custom_metrics[name] = value
end

.define_metric_group(group_name, metrics) ⇒ Object

Define a group of related metrics

Parameters:

  • group_name (Symbol)

    The name of the metric group

  • metrics (Array<Symbol>)

    The metrics that belong to this group



173
174
175
176
# File 'lib/action_mcp/log_subscriber.rb', line 173

def self.define_metric_group(group_name, metrics)
  self.metric_groups ||= {}
  self.metric_groups[group_name] = metrics
end

.format_metricsObject

Format metrics for display in logs



93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
# File 'lib/action_mcp/log_subscriber.rb', line 93

def self.format_metrics
  return nil if custom_metrics.nil? || custom_metrics.empty?

  # If grouping is enabled, organize metrics by groups
  if metric_groups.present?
    grouped_metrics = {}

    # Initialize groups with empty arrays
    metric_groups.each_key do |group_name|
      grouped_metrics[group_name] = []
    end

    # Add "other" group for ungrouped metrics
    grouped_metrics[:other] = []

    # Assign metrics to their groups
    custom_metrics.each do |key, value|
      group = nil

      # Find which group this metric belongs to
      metric_groups.each do |group_name, metrics|
        if metrics.include?(key)
          group = group_name
          break
        end
      end

      # Format the metric
      formatter = formatters&.dig(key)
      formatted_value = if formatter.respond_to?(:call)
                          formatter.call(value)
      elsif value.is_a?(Float)
                          format("%.1fms", value)
      else
                          value.to_s
      end

      formatted_metric = "#{key}: #{formatted_value}"

      # Add to appropriate group (or "other")
      if group
        grouped_metrics[group] << formatted_metric
      else
        grouped_metrics[:other] << formatted_metric
      end
    end

    # Join metrics within groups, then join groups
    grouped_metrics.map do |_group, metrics|
      next if metrics.empty?

      metrics.join(" | ")
    end.compact.join(" | ")
  else
    # No grouping, just format all metrics
    custom_metrics.map do |key, value|
      formatter = formatters&.dig(key)
      formatted_value = if formatter.respond_to?(:call)
                          formatter.call(value)
      elsif value.is_a?(Float)
                          format("%.1fms", value)
      else
                          value.to_s
      end
      "#{key}: #{formatted_value}"
    end.join(" | ")
  end
end

.measure_metric(name) ⇒ Object

Measure execution time of a block and add as metric



41
42
43
44
45
46
47
48
# File 'lib/action_mcp/log_subscriber.rb', line 41

def self.measure_metric(name)
  start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
  result = yield
  duration = (Process.clock_gettime(Process::CLOCK_MONOTONIC) - start_time) * 1000.0

  add_metric(name, duration)
  result
end

.register_formatter(metric_name, &block) ⇒ Object

Register a custom formatter for a specific metric

Parameters:

  • metric_name (Symbol)

    The name of the metric

  • block (Proc)

    The formatter block that takes the value and returns a string



165
166
167
168
# File 'lib/action_mcp/log_subscriber.rb', line 165

def self.register_formatter(metric_name, &block)
  self.formatters ||= {}
  self.formatters[metric_name] = block
end

.reset_metricsObject

Reset all custom metrics



51
52
53
# File 'lib/action_mcp/log_subscriber.rb', line 51

def self.reset_metrics
  self.custom_metrics = nil
end

.reset_runtimeObject



10
11
12
13
14
15
16
17
18
19
20
21
22
# File 'lib/action_mcp/log_subscriber.rb', line 10

def self.reset_runtime
  # Get the combined runtime from both tool and prompt operations
  tool_rt = Thread.current[:mcp_tool_runtime] || 0
  prompt_rt = Thread.current[:mcp_prompt_runtime] || 0
  total_rt = tool_rt + prompt_rt

  # Reset both counters
  Thread.current[:mcp_tool_runtime] = 0
  Thread.current[:mcp_prompt_runtime] = 0

  # Return the total runtime
  total_rt
end

.subscribe_event(pattern, metric_name, options = {}) ⇒ Object

Subscribe to a Rails event to capture metrics

Parameters:

  • pattern (String)

    Event name pattern (e.g., “sql.active_record”)

  • metric_name (Symbol)

    Name to use for the metric

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

    Options for capturing the metric



59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
# File 'lib/action_mcp/log_subscriber.rb', line 59

def self.subscribe_event(pattern, metric_name, options = {})
  self.subscribed_events ||= {}

  # Store subscription info
  self.subscribed_events[pattern] = {
    metric_name: metric_name,
    options: options
  }

  # Create the actual subscription
  ActiveSupport::Notifications.subscribe(pattern) do |*args|
    event = ActiveSupport::Notifications::Event.new(*args)

    # Extract value based on options
    value = if options[:duration]
              event.duration
    elsif options[:extract_value].respond_to?(:call)
              options[:extract_value].call(event)
    else
              1 # Default to count
    end

    # Accumulate or set the metric
    if options[:accumulate]
      self.custom_metrics ||= {}
      self.custom_metrics[metric_name] ||= 0
      self.custom_metrics[metric_name] += value
    else
      add_metric(metric_name, value)
    end
  end
end

Instance Method Details

#process_action(event) ⇒ Object

Enhance process_action to include our custom metrics



179
180
181
182
183
184
185
186
187
# File 'lib/action_mcp/log_subscriber.rb', line 179

def process_action(event)
  return unless logger.info?

  return unless self.class.custom_metrics.present?

  metrics_msg = self.class.format_metrics
  event.payload[:message] = "#{event.payload[:message]} | #{metrics_msg}" if metrics_msg
  self.class.reset_metrics
end

#prompt_call(event) ⇒ Object



29
30
31
32
# File 'lib/action_mcp/log_subscriber.rb', line 29

def prompt_call(event)
  Thread.current[:mcp_prompt_runtime] ||= 0
  Thread.current[:mcp_prompt_runtime] += event.duration
end

#tool_call(event) ⇒ Object



24
25
26
27
# File 'lib/action_mcp/log_subscriber.rb', line 24

def tool_call(event)
  Thread.current[:mcp_tool_runtime] ||= 0
  Thread.current[:mcp_tool_runtime] += event.duration
end