Logstash是一种软件工具,可被用来收集来自各种源(各种协议、格式、产出源)的日志数据并做过滤处理,然后将日志发送到指定位置(如文档、数据库、搜索引擎等)。
从技术研究角度来讲,以下关于Logstash的明显特征值得关注:
程序代码为Jruby语言所写。
Logstash的软件架构是一种带有“管道-过滤器”风格的插件式架构。
对于使用者来讲,Logstash本身是基于命令行界面,面向任务处理的。
以下,对上述特征作以解释说明。
Logstash的代码为JRuby,首先从与Ruby的兼容性来讲,其中的代码作少量修改基本就可以运行于一般的Ruby运行时,如MRI。另外,更重要的是,由于Jruby的运行于JVM中,那么它与Java就具备了天然的关联性。这为Java程序的集成工作提供了便利的基础。
Logstash的license是<code>Apache 2.0</code>,它本身是开放的,而且其目前的发展有一部分完全是第三方社区贡献的。这种发展局面的技术基础就是其开放的技术架构风格——插件式架构。定制、改造或者添加功能,仅需要提供新的插件即可。
Logstash的运行态是基于命令行界面,面向任务处理的。详细点说,使用者需要使用Logstash定义的DSL来创建任务配置文件,在该文件里定义日志数据处理涉及的插件与它们的组合方式,Logstash的启动程序会根据该配置来初始化运行时部件并处理数据。
1.Logstash的插件架构与运行时模型
正如前所述,Logstash首先是带有一种“管道-过滤器”架构风格。
<code>< Filter> |< Filter> |< Filter> ...|< Filter> </code>
那么上述抽象模型在Logstash中的具体实现为:
<code>input|filter|output</code>
好吧,上述说法基本上是官方的文档给出的说明。笔者的研究给出下面更详细的结果。
上图基本展示了Logstash的运行时模型概貌。详细解释如下:
- **数据处理阶段:**数据处理依次经历了三个阶段:“输入”、“过滤”与“输出”。其中“输入”与“输出”是必须有的,“过滤”阶段是可选的。也就是说,输入阶段的产出结果直接传输给输出阶段,这种情形下,模型中的两个数据缓冲队列<code>queue< input_to_filter> </code> 与<code>queue< filter_to_output> </code>就合二为一,变成一个<code>queue< input_to_output> </code>了。
- 插件类型:在每个阶段中,完成具体处理任务的就是插件了。且看,插件其实并不限于“input”、“filter”、“output”三种,还有一种“codec”类型的插件。所以说,logstash的数据处理流程其实是:<code>input|codec.decode|filter|codec.encode|output</code>。只是codec插件相较于input与output来讲,其没有独立执行任务的能力,它一般由上述两种插件引用工作。所以,Logstash的文档通常忽略了该插件在数据处理流程中所占的位置,仅粗略地将其隐式地纳入到被引用的范畴中去了。
- **线程模型:**关于其中的线程模型,首要说明的是,三个阶段的处理任务是异步的,不存在跨阶段任务执行与同一个线程中的情况。各阶段间的任务执行依靠中间的<code>queue</code>来进行。这里的queue其实就是ruby中的Queue类(定义于thread.rb)实例,它的功能在于可充当线程间FIFO的通路。若某线程想要读取空的queue时,它将被挂起,一旦queue中出现信息时,它将被重新开启。其次,在每个不同的阶段里,任务执行的线程模型是有区别的。
- 3.1 **对于input插件:**Logstash会使得每个插件工作于自己独立的线程空间中。
- 3.2 **对于filter插件:**Logstash默认会启动一个线程来执行filter任务,filter的作用顺序(执行顺序)遵循配置顺序。但,Logstash给予用户机会配置此处的线程数量。
- 3.3 **对于output插件:**Logstash目前应用的是单线程策略。也就是说所有的output插件实例都执行在同一个线程上下文中,顺序处理。不过,此处需要说明的是,Logstash不限定插件内部的实现策略,事实上有很多output插件会在内部缓冲数据并且管理自身的输出线程.
以上说明中都不涉及codec插件,因为codec插件工作于对应的input或者output插件中,直白的说,对于input或者output来说,codec是被当作工具辅助类对象来使用的。
2.配置Logstash
上一节介绍了Logstash的运行时模型以及所涉相关概念。那么这种运行时的整体结构与各子部件是“如何创建”或者说“依据什么创建”的呢?
那么,同时前文也说明了*“对于使用者来讲,Logstash本身是基于命令行界面,面向任务处理的。”*
启动Logstash时,需要为其指定配置参数。关于该配置参数有以下几点说明:
- 该配置参数描述了Logstash的运行部件与工作方式。比如说,设定输入处理阶段的input插件为file插件(从指定的文件目录中监听文件tail更新)
- Logstash设计了自己的DSL专门用于设置该配置参数。该配置参数可以直接写在由命令行中;也可以写在单独的文件里,在命令行中传递该文件路径即可。
- Logstash会依据该配置启动一个工作进程。目前没有动态更新配置的功能,若变更配置需要从新配置重新启动。
一个简单的配置示例如下:
input {
filex {
path => ["D:\Dev\logstash-1.4.2\bin\inputs\access.log"]
}
}
output {
stdout {
codec=>rubydebug
}
}
该配置为Logstash的*“输入处理”与“输出处理”进行了设定(无filter设置)。其中,设定“输入处理”*阶段的工作插件有一个,为<code>filex</code>插件,且该插件本身有一个文件路径参数。设定“输出处理”阶段的工作插件有一个,为<code>stdout</code>插件,且该插件使用一个叫做<code>rubydebug</code>的codec插件处理输出编码问题。
有关Logstash的DSL更详细的内容请参考:"Logstash Config Language"
3.启动与运行Logstash
3.1 启动脚本
以windows系统中启动脚本为例(%LS_HOME%\bin\logstash.bat),该脚本中的关键代码如下:
REM....
REM %LS_HOME%是Logstash的部署目录
set RUBYLIB=%LS_HOME%\lib
set GEM_HOME=%LS_HOME%\vendor\bundle\jruby\1.9\
set GEM_PATH=%GEM_HOME%
REM....
REM %RUBY_CMD% 的结构大概为 "%JAVA_HOME%\bin\java" -jar %LS_HOME%\vendor\jar\jruby-complete-*.jar
:run_logstash
%RUBY_CMD% "%LS_HOME%\lib\logstash\runner.rb" %*
可以看到,首先设置了三个环境变量:
- RUBYLIB
- GEM_HOME
- GEM_PATH
这三个环境变量是跟ruby运行时相关的。RUBYLIB设置了ruby中<code>require</code>语句寻找对应代码文件的查询路径;GEM_HOME与GEM_PATH设置了Gem库的位置。可见logstash不仅带了一个jruby的运行时jar,还自带了一套Gem库。对此,若要深入理解,请参照ruby相关资料。
无论如何,logstash的启动是从runner.rb开始的了。
3.2 runner.rb
runner.rb里的工作主要是处理启动参数,注意:这里所说的参数并非前文所述的运行配置,而是比如查看版本号(--version):
~:\logstash version
查看帮助(--help)之类的程序参数。
~:\logstash help
真正启动运行程序的参数是"agent",含义大概就是命令logstash以agent形式运行起来吧。比如:
~:\logstash agent -e input{stdin{}}output{stdout{}}
上述命令就真正地把logstash运行了起来。(-e 选项后紧跟了“运行配置”)
且看,runner.rb中是如何处理agent参数的:
"agent" => lambda do
require "logstash/agent"
# Hack up a runner
agent = LogStash::Agent.new($0)
begin
agent.parse(args)
rescue Clamp::HelpWanted => e
show_help(e.command)
return []
rescue Clamp::UsageError => e
# If 'too many arguments' then give the arguments to
# the next command. Otherwise it's a real error.
raise if e.message != "too many arguments"
remaining = agent.remaining_arguments
end
@runners << Stud::Task.new { agent.execute }
return remaining
end
可以看到,它首先创建了一个<code>agent</code>对象:<code>agent = LogStash::Agent.new($0)</code>;之后将参数传递给了它的<code>parse</code>方法:<code>agent.parse(args)</code>。
那么剩下的工作就全在agent对象里了。接下来,再去看一下<code>Agent</code>类的定义。
3.2 agent.rb
agent.rb中首先当然也是处理参数了。这里面使用了一个Gem库<code>clamp</code>来处理了命令参数,该部分不细表。
按照执行顺序,依次来看,比较重要的代码片段如下:
片段1:
# You must specify a config_string or config_path
if @config_string.nil? && @config_path.nil?
fail(help + "\n" + I18n.t("logstash.agent.missing-configuration"))
end
@config_string = @config_string.to_s
if @config_path
# Append the config string.
# This allows users to provide both -f and -e flags. The combination
# is rare, but useful for debugging.
@config_string = @config_string + load_config(@config_path)
else
# include a default stdin input if no inputs given
if @config_string !~ /input *{/
@config_string += "input { stdin { type => stdin } }"
end
# include a default stdout output if no outputs given
if @config_string !~ /output *{/
@config_string += "output { stdout { codec => rubydebug } }"
end
end
上方代码中的<code>@config_string</code>就是传进来的运行配置选项啦。可以看到,该配置不能为空(<code>nil</code>),是必须配置项。
然后由于该配置既可以是从参数值中内联传入的的也可以指定文件的,所以做了一些处理(如果是文件路径,就加载指定文件;而且两种配置还可以进行拼接)。
然后,参数值里是有缺省值的。缺省就是:<code>input { stdin { type => stdin } } output { stdout { codec => rubydebug } }</code>。如果只有选项名,但是没有选项值;或者比如只配input没有配output的情况就会被缺省值顶上。
片段2:
begin
pipeline = LogStash::Pipeline.new(@config_string)
rescue LoadError => e
fail("Configuration problem.")
end
处理完运行配置值,接下来,创建了一个pipeline对象,该对象的初始化参数就是方才确定的运行配置值。
片段3:
pipeline.run
创建完成后,接下来执行了<code>pipeline</code>对象的<code>run</code>方法。到此<code>agent</code>的主要工作已经完成,接下来的任务已经交给了<code>pipeline</code>对象。
3.3 pipeline.rb
首先,来看一下<code>pipeline</code>对象的初始化方法中都做了些什么。
def initialize(configstr)
grammar = LogStashConfigParser.new
@config = grammar.parse(configstr)
# This will compile the config to ruby and evaluate the resulting code.
# The code will initialize all the plugins and define the
# filter and output methods.
code = @config.compile
begin
eval(code)
rescue => e
raise
end
@input_to_filter = SizedQueue.new(20)
# If no filters, pipe inputs directly to outputs
if !filters?
@filter_to_output = @input_to_filter
else
@filter_to_output = SizedQueue.new(20)
end
@settings = {
"filter-workers" => 1,
}
end # def initialize
记得<code>agent</code>对象中创建<code>pipeline</code>对象时传入了运行配置参数。即上方代码中<code>initialize</code>方法的<code>configstr</code>参数。该参数经过<code>parse</code>与<code>compile</code>后被最终处理为<code>code</code>变量,之后被<code>eval</code>执行。以第二小节中的运行配置为例,被解析编译后变成的<code>code</code>如下所示:
@inputs = []
@filters = []
@outputs = []
@input_filex_1 = plugin("input", "filex", LogStash::Util.hash_merge_many({ "path" => [("D:\\Dev\\logstash-1.4.2\\bin\\inputs\\access.log".force_encoding("UTF-8"))] }, { "type" => ("system".force_encoding("UTF-8")) }))
@inputs << @input_filex_1
@output_stdout_2 = plugin("output", "stdout", LogStash::Util.hash_merge_many({ "codec" => ("rubydebug".force_encoding("UTF-8")) }))
@outputs << @output_stdout_2
@filter_func = lambda do |event, &block|
extra_events = []
@logger.debug? && @logger.debug("filter received", :event => event.to_hash)
extra_events.each(&block)
end
@output_func = lambda do |event, &block|
@logger.debug? && @logger.debug("output received", :event => event.to_hash)
@output_stdout_2.handle(event)
end
该部分代码就是依据运行配置生成了运行时对应的程序部件包括:设定的各种插件(<code>@input_filex_1</code>、<code>@output_stdout_2</code>)以及组织这些插件的数组(<code>@inputs</code>数组、<code>@filters</code>数组、<code>@outputs</code>数组)。
并且,pipeline接下来的代码中准备了两个关键的缓存队列:
- <code>@input_to_filter = SizedQueue.new(20)</code>
- <code>@filter_to_output = SizedQueue.new(20)</code>
如果没有设置filter插件的话,就直接作<code>@filter_to_output = @input_to_filter</code>短接处理了。
最后还有一个设定<code>"filter-workers" => 1</code>。即第一小节中关于filter的线程模型中所讲:
**对于filter插件:**Logstash默认会启动一个线程来执行filter任务,filter的作用顺序(执行顺序)遵循配置顺序。但,Logstash给予用户机会配置此处的线程数量。
<code>pipeline</code>对象在初始化方法里,做了以上准备工作。可见,Logstash的运行时所需的各种部件都是在这里准备好的。接下来的工作就是启动各个工作子线程,让各种部件工作起来。记得<code>agent</code>对象中最终调用了<code>pipeline</code>对象的<code>run</code>方法。没错,就在这个<code>run</code>方法里,启动了各运行时部件。关键代码如下:
def run
start_inputs
start_filters if filters?
start_outputs
wait_inputs
if filters?
shutdown_filters
wait_filters
end
shutdown_outputs
wait_outputs
@logger.info("Pipeline shutdown complete.")
# exit code
return 0
end # def run
上述逻辑大概就是:
- 启动运行input插件;
- 如果有filter插件,那么启动运行filter插件;
- 启动运行output插件;
- 等待input插件线程(这里是对所有input插件的执行线程调用join方法,进而各个子线程将会阻塞主线程的执行,直到子线程结束);
- input线程都已经结束后,试图关闭filter线程,并等待结束;
- 接着再试图关闭output线程,并等待结束。
以上内容基本上描述了Logstash的主线程逻辑。
4.扩展Logstash
扩展Logstash的方式主要就是为其编写新的插件。关于Logstash的插件,有那么几个相关类,如下图所示。
如上图所示,所有插件都会有一个最根本的父类:<code>Logstash::Plugin</code>。该类中主要定义了一些插件声明周期相关的内容,包括状态定义与诸如<code>finish</code>、<code>shutdown</code>这样的状态转换操作,还有一些插件加载寻址的框架代码。这里面的生命周期操作目前基本不用关心,这在Logstash里并没有被用到关键之处。而插件加载也不用关心,它只是去加载相应代码而已,只管按照约定去用就行。
而三大类插件分别对应了四种进一步具化的基类,分别为:
- <code>Logstash::Inputs::Base</code>
- <code>Logstash::Outputs::Base</code>
- <code>Logstash::Filters::Base</code>
- <code>Logstash::Codecs::Base</code>
对于最终具体的插件实现,分别实现上述对应的基类即可。接下来主要以Input插件为例进行进一步说明。
4.1 自定义Input插件
一个标准的 logstash 输入插件格式如下:
require 'logstash/namespace'
require 'logstash/inputs/base'
class LogStash::Inputs::MyPlugin < LogStash::Inputs::Base
config_name 'myplugin'
milestone 1
config :myoption_key, :validate => :string, :default => 'myoption_value'
public def register
end
public def run(queue)
end
end
其中大多数语句在过滤器和输出阶段是共有的。
- config_name: 用来定义该插件写在 logstash 配置文件里的名字;
- milestone: 标记该插件的开发里程碑,一般为1,2,3,如果不再维护的,标记为 0;
- config: 可以定义很多个,即该插件在 logstash 配置文件中的可配置参数。logstash 很温馨的提供了验证方法,确保接收的数据是你期望的数据类型;
- register: logstash 在启动的时候运行的函数,一些需要常驻内存的数据,可以在这一步先完成。比如对象初始化,filters/ruby 插件中的 init 语句等。
注意:milestone 级别在 3 以下的,logstash 默认为不足够稳定,会在启动阶段,读取到该插件的时候,输出类似下面这样的一行提示信息,日志级别是 warn。这不代表运行出错!只是提示如果用户碰到 bug,欢迎提供线索。
代码中的 run 方法是input插件独有的。在 run 方法中,该方法的参数<code>queue</code>就是前文中提到的<code>input_to_filter_queue</code>了,主线程中的控制代码会将该队列传进来。所以<code>run</code>方法中收到数据并处理成 event 之后,一定要调用 queue << event 语句。如此一个输入流程就算是完成了。
比如,下方大代码是自定义的<code>filex</code>插件代码。
# encoding: utf-8
require "logstash/inputs/base"
require "logstash/namespace"
require "socket" # for Socket.gethostname
# Stream events from files.
class LogStash::Inputs::Filex < LogStash::Inputs::Base
config_name "filex"
milestone 2
default :codec, "plain"
config :path, :validate => :array, :required => true
public
def register
@logger.info("Registering filex input", :path => @path)
end # def register
public
def run(queue)
hostname = Socket.gethostname
files = path.each.collect do |path|
File.open(path,"r")
end
files.each do |file|
file.each_line do |line|
@logger.debug? && @logger.debug("Received line", :path => File.basename(file), :text => line)
@codec.decode(line) do |event|
decorate(event)
event["host"] = hostname if !event.include?("host")
event["path"] = File.absolute_path(file)
queue << event
end
end
file.close
end
finished
end # def run
public
def teardown
end # def teardown
end # class LogStash::Inputs::File
4.2 自定义filter、output与codec插件
相比较与输入插件,如果是过滤器插件,对应修改成:
require 'logstash/filters/base'
class LogStash::Filters::MyPlugin < LogStash::Filters::Base
public def filter(event)
end
end
而输出插件则是:
require 'logstash/outputs/base'
class LogStash::Outputs::MyPlugin < LogStash::Outputs::Base
public def receive(event)
end
end
另外,无论是input、output还是filter插件,都有一点:为了在终止进程的时候不遗失数据,建议都实现如下这个方法,只要实现了,logstash 的主线程中在 shutdown 的时候就会自动调用:
public def teardown
end
最后还有一种codec插件,如下:
require "logstash/codecs/base"
class LogStash::Codecs::MyPlugin < LogStash::Codecs::Base
public def decode(data)
end # def decode
public def encode(data)
end # def encode
end
从上述代码框架来看,codec基本就是充当工具类的作用。