在 Logstash 中使用 Ruby 脚本

作者:来自 Elastic Dai Sugimori

了解 Logstash Ruby filter 插件,在你的 Logstash pipeline 中进行高级数据转换。

更多阅读: Logstash:使用 Ruby 过滤器

了解将数据导入 Elasticsearch 的不同方式,并深入实际示例,尝试一些新方法。

Elasticsearch 拥有丰富的新功能,帮助你为你的使用场景构建最佳的搜索解决方案。立即开始免费试用


Logstash 是一个数据处理管道,它从多个来源摄取数据,进行转换,并将其发送到你选择的目标位置。Filter 插件是这个过程的关键,它们在数据通过管道时执行特定的操作。

Logstash 包含多个内置的 filters,用于解析、丰富和修改数据等常见任务。但有时你会遇到需要超出这些标准 filter 功能的自定义逻辑的情况,这时就需要使用 Ruby filter 插件

Ruby filter 插件允许你在 Logstash pipeline 中直接执行自定义的 Ruby 代码。当标准 filter 无法满足需求时,Ruby filter 能让你处理复杂的数据转换、实现自定义的业务逻辑,或与外部系统集成。

在这篇博客中,我们将探索如何使用 Ruby filter,从基础用法到高级用法。

什么时候应该使用 Ruby filter?

作为 Elastic 的咨询架构师,我经常看到客户在数据处理管道中使用 Logstash,尽管如今它已不是最先进的数据处理引擎。他们在处理复杂数据或自定义逻辑时,常常受限于标准 filter 的能力。在这种情况下,Ruby filter 可以帮助克服这些挑战。

当标准的 Logstash filter 无法满足你特定的需求时,Ruby filter 就非常有用。以下是一些常见的使用场景:

  • 深度嵌套数据处理:修改复杂的 JSON 结构、嵌套数组,或根据内容动态重构数据

  • 高级字符串处理:从非结构化文本中解析并提取结构化数据

  • 实现复杂的业务逻辑:创建需要条件判断、循环或复杂计算的自定义转换

基本用法

我们先从一个简单的例子开始,了解 Ruby filter 是如何工作的。

配置 Ruby filter

当你创建一个 Logstash pipeline 时,应该将配置文件放在 /etc/logstash/conf.d 目录下。或者,你也可以使用 -f 选项在手动启动 Logstash 时指定配置文件路径,这样可以更方便地实验你的 pipeline。

$ ./bin/logstash -f /path/to/your_pipeline.conf

配置文件应以 .conf 作为扩展名。

要使用 Ruby filter,需要在 Logstash pipeline 配置文件(*.conf)的 filter 部分中定义一个 ruby filter。下面是一个基本示例:

filter {
  ruby {
    code => "
      event.set('new_field', 'Hello from Ruby!')
    "
  }
}

这个内联 Ruby filter 在你的 Logstash 配置中定义了一个 Ruby filter 实例。code 参数提供了内联的 Ruby 脚本,Logstash 会对每个通过该 filter 的事件执行这段代码。在这段脚本中,有一个名为 event 的变量,代表当前处理的事件。event 对象包含发送到 Logstash 的原始数据,以及在 Logstash 的 filter 阶段创建的其他字段。你可以通过 Logstash 的 Event API(例如 event.get()event.set())访问这些字段。

在这个示例代码中,event.set('new_field', 'Hello from Ruby!') 将一个名为 new_field 的新字段设置为字符串值 Hello from Ruby!。你可以根据需要在这个代码块中添加其他逻辑。

请注意,虽然 event 对象表现得像一个键值对容器,但它并不是普通的 Ruby hash 对象。想了解更多关于 Event API 的信息,可以参考官方文档

提取 Ruby 脚本到外部文件

对于简单的转换,使用内联 Ruby 代码很方便。但对于复杂逻辑或可复用的函数,推荐将代码移动到外部 Ruby 脚本中。这样有助于提高可维护性,并保持你的 Logstash pipeline 配置简洁。

首先,创建一个 Ruby 脚本并将其保存为 my_ruby_script.rb。该脚本必须定义一个名为 filter 的方法,用来处理事件。它接收一个 event 对象作为参数,代表当前正在处理的事件。filter 方法需要返回一个事件数组作为输出。若要丢弃该事件,则返回一个空数组。

例如,下面的脚本读取 message 字段,计算其长度,并将结果存储到一个新字段 message_length 中。

def register(params)
  # This method is called when the plugin is loaded.
  # You can use it to initialize any instance variables or perform setup tasks.
end

def filter(event)
  message = event.get('message')

  if message
    event.set('message_length', message.length)
  end

  return [event]
end

接下来,在 Ruby filter 配置中使用 path 选项引用该脚本。这会告诉 Logstash 加载并执行这个外部脚本。当使用外部脚本时,请确保该文件存在并具有正确的权限。

filter {
  ruby {
    path => "/path/to/my_ruby_script.rb"
  }
}

现在,每个事件都会传递给 my_ruby_script.rb 中的 filter 方法进行处理。

这种方式有助于你更高效地管理复杂逻辑,使你的 Ruby 代码更易于测试、调试和复用。

高级用法

在本节中,我们将探索一些在 Logstash 中使用 Ruby filter 的高级示例。这些示例将展示如何使用 Ruby 进行数据转换、丰富事件以及实现自定义逻辑。

操作嵌套数据结构

Logstash 事件是 Logstash 处理的核心数据结构。它可以包含各种字段,包括像数组和哈希这样的嵌套数据结构。Ruby filter 允许你轻松操作这些嵌套结构。

Ruby filter 能处理嵌套的数据结构,比如哈希和数组,使你可以修改或添加这些结构中的字段。这在处理像 JSON 这样的复杂数据格式时非常有用。

input {
  generator {
    lines => [
      '{"nested": {"key1": "value1", "key2": "value2"}}'
    ]
    count => 1
    codec => "json"
    ecs_compatibility => "disabled"
  }
}

filter {
  ruby {
    code => "
      nested_data = event.get('nested')

      if nested_data.is_a?(Hash)
        nested_data['key3'] = 'value3'
        event.set('nested', nested_data)
      end
    "
  }
}

output {
  stdout { codec => rubydebug }
}

这个示例中,输入数据包含一个嵌套的 JSON 对象。Ruby filter 修改了嵌套数据,添加了一个新的键值对。标准的 Logstash filter 无法完成这种嵌套数据的操作,因此 Ruby filter 是处理复杂数据结构的实用工具。

将一个事件拆分为多个事件

Ruby filter 也可以用来将单个事件拆分成多个事件。当一个事件中包含一个数组,而你希望为数组中的每个项创建一个独立事件时,这种方式非常有用。

需要注意的是,Elasticsearch 的 ingest pipeline 以及 Beats / Elastic Agent 的 processors 都不支持事件拆分,这是 Logstash 的一个强大用例。

使用 split filter

你可以使用 split filter 根据指定字段将一个事件拆分为多个事件。但如果在拆分过程中还需要执行额外的转换或逻辑处理,则可以将 Ruby filter 与 split filter 结合使用。

在下面的示例中,我们有一个 RSS feed,它是一行 XML 文本,其中包含多个 <item> 元素。Ruby filter 用于从 XML 中提取 <item> 元素,并将其存入一个新字段 items。随后,split filter 用于根据 items 字段将事件拆分成多个事件。

input {
  generator {
    lines => [
      '<rss version="2.0"><channel><title>Sample RSS</title><item><title>Article 1</title><link>http://example.com/1</link><description>Desc 1</description></item><item><title>Article 2</title><link>http://example.com/2</link><description>Desc 2</description></item></channel></rss>'
    ]
    count => 1
    codec => "plain"
    ecs_compatibility => "disabled"
  }
}

filter {
  xml {
    source => "message"
    target => "rss"
    store_xml => true
    force_array => false
  }
  ruby {
    code => "event.set('items', event.get('[rss][channel][item]')) if event.get('[rss][channel][item]')"
  }
  split {
    field => "items"
  }
  ruby {
    code => "
      item = event.get('items')
      event.set('title', item['title']) if item['title']
      event.set('link', item['link']) if item['link']
      event.set('description', item['description']) if item['description']
    "
  }
  mutate {
    remove_field => ["@timestamp", "@version", "sequence", "host", "event", "message", "rss", "items"]
  }
}

output {
  stdout { codec => rubydebug }
}

输出结果如下:

{
          "title" => "Article 1",
           "link" => "http://example.com/1",
    "description" => "Desc 1"
}
{
          "title" => "Article 2",
           "link" => "http://example.com/2",
    "description" => "Desc 2"
}

正如你可能注意到的,这种情况下 Ruby filter 并不是必需的。可以使用 split filter 根据 items 字段将事件拆分成多个事件,再用 mutate filter 删除不必要的字段。不过,如果你需要在拆分过程中执行额外的转换或逻辑,就可以使用 Ruby filter。

使用内联 Ruby 脚本

你也可以用内联 Ruby 脚本通过 event.clone 方法和 new_event_block 变量(例如 new_event_block.call(new_event))将单个事件拆分成多个事件。这允许你基于原事件创建新事件,同时保留原事件的数据。

下面是一个使用 Ruby filter 将单个事件拆分成多个事件的示例。输入和输出与前一个示例相同。

filter {
  xml {
    source => "message"
    target => "rss"
    store_xml => true
    force_array => false
  }
  ruby {
    code => "
      items = event.get('[rss][channel][item]')
      if items.is_a?(Array)
        items.each do |item|
          new_event = event.clone
          new_event.set('title', item['title'])
          new_event.set('link', item['link'])
          new_event.set('description', item['description'])
          new_event_block.call new_event
        end
        event.cancel
      elsif items.is_a?(Hash)
        event.set('title', items['title'])
        event.set('link', items['link'])
        event.set('description', items['description'])
      end
    "
  }
  mutate {
    remove_field => ["@timestamp", "@version", "sequence", "host", "event", "message", "rss", "items"]
  }
}

使用外部 Ruby 脚本

你也可以使用外部 Ruby 脚本将单个事件拆分成多个事件。

配置文件:

filter {
  xml {
    source => "message"
    target => "rss"
    store_xml => true
    force_array => false
  }
  ruby {
    path => "path/to/ruby/split_event.rb"
  }
  mutate {
    remove_field => ["@timestamp", "@version", "sequence", "host", "event", "message", "rss", "items"]
  }
}

Ruby 脚本需要外部保存为 split_event.rb:

def filter(event)
  items = event.get('[rss][channel][item]')
  events = []
  if items.is_a?(Array)
    items.each do |item|
      new_event = event.clone
      new_event.set('title', item['title'])
      new_event.set('link', item['link'])
      new_event.set('description', item['description'])
      events << new_event
    end
    return events
  elsif items.is_a?(Hash)
    event.set('title', items['title'])
    event.set('link', items['link'])
    event.set('description', items['description'])
    return [event]
  else
    return []
  end
end

请记住,filter 方法必须返回一个事件数组。你可以通过克隆传入的事件对象并将它们添加到数组中来返回多个事件,或者以包含一个元素的数组形式返回单个事件。

return events
# or
# return [event]

这使你可以将单个事件拆分成多个事件。

执行外部命令并解析其输出

Logstash 的 exec input 插件允许你执行外部命令,命令的输出将作为 Logstash 的一个事件。命令的输出内容会存储在事件的 message 字段中。

通常,系统命令的输出是可读的文本格式,而不是 Logstash 能够轻松解析的 JSON 或其他结构化格式。为了解决这个问题,你可以使用 Ruby filter 来解析输出并提取其中的信息。

下面是一个使用 exec input 插件执行 ps -ef 命令的示例,该命令列出类 Unix 系统上所有正在运行的进程。Ruby filter 会解析输出,提取每个进程的相关信息。

input {
  exec {
    command => "ps -ef"
    interval => 60
  }
}

filter {
  ruby {
    code => '
      processes = []
      lines = event.get("message").split("\n")  
      lines.each_with_index do |line, index|
        # Skip header line and empty lines
        next if index == 0 || line.strip.empty?
        entry = nil
        
        # Use regex to match the ps -ef output format more flexibly
        # This pattern accounts for variable spacing and different time formats
        if line =~ /^\s*(\S+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\S+)\s+(\S+)\s+([\d:]+\.?\d*)\s+(.+)$/
          uid, pid, ppid, c, stime, tty, time, cmd = $1, $2, $3, $4, $5, $6, $7, $8
          
          entry = {
            "UID" => uid,
            "PID" => pid,
            "PPID" => ppid,
            "C" => c,
            "STIME" => stime,
            "TTY" => tty,
            "TIME" => time,
            "CMD" => cmd.strip
          }
        elsif line =~ /^\s*(\S+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(.+)$/
          # Fallback pattern for lines that might not match the exact format
          # Split the remaining part more carefully
          uid, pid, ppid, c, remainder = $1, $2, $3, $4, $5
          
          # Split remainder into STIME, TTY, TIME, CMD
          parts = remainder.strip.split(/\s+/, 4)
          if parts.length >= 4
            stime, tty, time, cmd = parts[0], parts[1], parts[2], parts[3]
            
            entry = {
              "UID" => uid,
              "PID" => pid,
              "PPID" => ppid,
              "C" => c,
              "STIME" => stime,
              "TTY" => tty,
              "TIME" => time,
              "CMD" => cmd
            }
          end
        end
        if entry && entry["UID"] == "0"
          original_line = line.strip
          entry["original_line"] = original_line if original_line.length > 0
          processes.push(entry)
        end
      end
      event.set("processes", processes)
      event.remove("message")
      event.remove("event")
    '
  }
}

output {
  stdout { codec => rubydebug }
}

这个示例使用 exec input 插件每 60 秒运行一次 ps -ef 命令。Ruby filter 处理命令输出,提取相关字段,如 UID、PID、PPID、CPU 使用率(C)、启动时间(STIME)、TTY、总 CPU 时间(TIME)以及执行的命令(CMD)。它在我的 macOS 环境下运行良好,但你可能需要调整正则表达式以匹配你系统上 ps -ef 命令的输出格式。

使用内置库

Ruby filter 插件允许你使用 Ruby 的内置库,这对各种任务非常有用。例如,你可以使用 json 库解析 JSON 字符串,或使用 date 库处理日期。

下面是一个使用 json 库解析存储在字段中的 JSON 字符串的示例:

require 'json'

def filter(event)
  json_string = event.get('message')
  parsed_json = JSON.parse(json_string)
  event.set('parsed_json', parsed_json)
  return [event]
end

为了避免每次都调用 require,你应该将 Ruby 代码外部化,这样就能在 Ruby filter 脚本开头使用 require 语句。这会让库只加载一次,并在脚本中可用。

要检查你环境中有哪些库可用,可以在 Ruby filter 中运行以下代码列出内置库:

Gem.loaded_specs.sort_by { |name, _| name }.each do |name, spec|
  puts "#{name}: #{spec.version}"
end

注意:内置库并非 Logstash 官方支持,它们的行为可能会变化,或者在未来版本中不可用。使用时请自行承担风险。

总结

Logstash Ruby filter 让你能够自定义并扩展 Logstash pipeline 的功能。本文介绍了 Ruby filter 的基础用法,并提供了高级示例。

通过利用 Ruby filter,你可以处理需要自定义逻辑或高级操作的复杂数据处理任务。无论是操作嵌套数据结构、拆分事件,还是解析并转换复杂或非结构化文本为结构化 JSON,Ruby filter 都能提供灵活性以满足你的具体需求。

希望这份指南能帮助你了解并激发探索 Logstash Ruby filter 的全部潜力。祝你编写脚本愉快!

原文:Ruby scripting in Logstash - Elasticsearch Labs

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值