ようへいの日々精進XP

よかろうもん

小ネタ道場一本勝負 〜 初体験 Ruby でメソッドの動的生成 〜

なんしよーとや

awspec の CloudWatch Logs Resource Type の Generator を作ろうとしています。

github.com

一応、Pull request 済みです。

github.com

さて、今回

Metric Filter と Subscription Filter を取得するメソッド

Log group をキーとして Metric Filter と Subscription Filter を取得するメソッドを以下のように書いていました。

def generate_log_metric_filters_specs(log_group)
  req = { log_group_name: log_group }
  metric_filters = []
  loop do
    res = cloudwatch_logs_client.describe_metric_filters(req)
    metric_filters.push(*res.metric_filters)
    break if res.next_token.nil?
    req[:next_token] = res.next_token
  end
  metric_filter_lines = []
  metric_filters.each do |metric_filter|
    line = "it { should have_metric_filter('#{metric_filter.filter_name}') }"
    metric_filter_lines.push(line)
  end
  metric_filter_lines
end

def generate_log_subscription_filters_specs(log_group)
  req = { log_group_name: log_group }
  subscription_filters = []
  loop do
    res = cloudwatch_logs_client.describe_subscription_filters(req)
    subscription_filters.push(*res.subscription_filters)
    break if res.next_token.nil?
    req[:next_token] = res.next_token
  end
  subscription_filter_lines = []
  subscription_filters.each do |subscription_filter|
    line = "it { should have_subscription_filter('#{subscription_filter.filter_name}')"
    unless subscription_filter.filter_pattern.empty?
      line += ".filter_pattern('#{subscription_filter.filter_pattern}')"
    end
    line += ' }'
    subscription_filter_lines.push(line)
  end
  subscription_filter_lines
end

なんと無く冗長です。

どこが冗長なん?

インフラでは冗長化は当たり前の事かもしれませんが、プログラミングでは冗長はあまり好まれないと考えています。

ということで、個人的には以下の部分が冗長だと思っています。

...

  req = { log_group_name: log_group }
  metric_filters = []
  loop do
    res = cloudwatch_logs_client.describe_metric_filters(req)
    metric_filters.push(*res.metric_filters)
    break if res.next_token.nil?
    req[:next_token] = res.next_token
  end

...

  req = { log_group_name: log_group }
  subscription_filters = []
  loop do
    res = cloudwatch_logs_client.describe_subscription_filters(req)
    subscription_filters.push(*res.subscription_filters)
    break if res.next_token.nil?
    req[:next_token] = res.next_token
  end
...

これらの処理をうまーく、一つの処理にまとめられればメソッドの行数は減らせて冗長を解消出来そうです。

ということで、一本

define_method を使ってメソッドを動的に生成することにした

以下はサンプル実装です。

require 'aws-sdk'
require 'pp'

cloudwatch_logs_client = Aws::CloudWatchLogs::Client.new(profile: 'your-profile', region: 'ap-northeast-1')

filter_types = %w(metric subscription)
filter_types.each do |type|
  define_method 'select_all_cloudwatch_logs_' + type + '_filter' do |*args|
    req = { log_group_name: args.first }
    method_name = 'describe_' + type + '_filters'
    resources = []
    loop do
      res = cloudwatch_logs_client.method(method_name).call(req)
      case type
      when 'metric' then
        resources.push(*res.metric_filters)
      when 'subscription' then
        resources.push(*res.subscription_filters)
      end
      break if res.next_token.nil?
      req[:next_token] = res.next_token
    end
    resources
  end
end

pp select_all_cloudwatch_logs_metric_filter('my_log_group_name')
pp select_all_cloudwatch_logs_subscription_filter('my_log_group_name')

ポイントは define_method ですな。実行すると以下のようなメソッドに展開されると妄想しています。

def select_all_cloudwatch_logs_metric_filter(log_group)
  req = { log_group_name: log_group }
  metric_filters = []
  loop do
    res = cloudwatch_logs_client.describe_metric_filters(req)
    metric_filters.push(*res.metric_filters)
    break if res.next_token.nil?
    req[:next_token] = res.next_token
  end
end
...

def select_all_cloudwatch_logs_subscription_filter(log_group)
  req = { log_group_name: log_group }
  subscription_filters = []
  loop do
    res = cloudwatch_logs_client.describe_subscription_filters(req)
    subscription_filters.push(*res.subscription_filters)
    break if res.next_token.nil?
    req[:next_token] = res.next_token
  end
end

実際に実行してみると以下のように出力されました。

$ bundle exec ruby test.rb
[#<struct Aws::CloudWatchLogs::Types::MetricFilter
  filter_name="my_metric_filter",
  filter_pattern="OK",
  metric_transformations=
   [#<struct Aws::CloudWatchLogs::Types::MetricTransformation
     metric_name="OK_Test",
     metric_namespace="LogMetrics",
     metric_value="1",
     default_value=nil>],
  creation_time=1491118453323,
  log_group_name="my_log_group_name">]
[#<struct Aws::CloudWatchLogs::Types::SubscriptionFilter
  filter_name="my_subscription_filter",
  log_group_name="my_log_group_name",
  filter_pattern="",
  destination_arn=
   "arn:aws:lambda:ap-northeast-1:xxxxxxxxxxxx:function:elb_log_to_amazones",
  role_arn=nil,
  distribution="ByLogStream",
  creation_time=1491092185825>]

意図した通りの結果が返ってきています。

あざっした

メソッドの動的生成は初めての体験でした。

define_method を適材適所で使うことで柔軟で且つ少ないコードで処理を実装出来るって素晴らしいですね。

Ruby すごなあ。