test-jq 0.5.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/CHANGES.md +4 -0
- data/CONTRIBUTING.md +11 -0
- data/Gemfile +3 -0
- data/Gemfile.lock +70 -0
- data/LICENSE +202 -0
- data/Makefile +10 -0
- data/README.md +214 -0
- data/Rakefile +25 -0
- data/VERSION +1 -0
- data/example/Dockerfile +19 -0
- data/example/fluent.conf +42 -0
- data/example/statsd.sample +4 -0
- data/fluent-plugin-jq.gemspec +28 -0
- data/lib/fluent/plugin/filter_jq_transformer.rb +41 -0
- data/lib/fluent/plugin/formatter_jq.rb +57 -0
- data/lib/fluent/plugin/jq_mixin.rb +50 -0
- data/lib/fluent/plugin/out_jq.rb +52 -0
- data/lib/fluent/plugin/parser_jq.rb +42 -0
- data/run_ci.sh +5 -0
- data/test/helper.rb +10 -0
- data/test/plugin/test_filter_jq_transformer.rb +74 -0
- data/test/plugin/test_formatter_jq.rb +57 -0
- data/test/plugin/test_out_jq.rb +58 -0
- data/test/plugin/test_parser_jq.rb +48 -0
- metadata +163 -0
data/Rakefile
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
require "bundler"
|
2
|
+
Bundler::GemHelper.install_tasks
|
3
|
+
|
4
|
+
require "rake/testtask"
|
5
|
+
|
6
|
+
task :build_example do
|
7
|
+
sh 'docker build -t fluent-plugin-jq-example -f example/Dockerfile .'
|
8
|
+
end
|
9
|
+
|
10
|
+
task :rm_example do
|
11
|
+
sh 'docker image rm fluent-plugin-jq-example'
|
12
|
+
end
|
13
|
+
|
14
|
+
task :run_example do
|
15
|
+
sh 'docker run -it --rm fluent-plugin-jq-example'
|
16
|
+
end
|
17
|
+
|
18
|
+
Rake::TestTask.new(:test) do |t|
|
19
|
+
t.libs.push("lib", "test")
|
20
|
+
t.test_files = FileList["test/**/test_*.rb"]
|
21
|
+
t.verbose = true
|
22
|
+
t.warning = true
|
23
|
+
end
|
24
|
+
|
25
|
+
task default: [:test]
|
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
0.5.1
|
data/example/Dockerfile
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
FROM fluent/fluentd:v1.1.0
|
2
|
+
LABEL maintainer="Zhimin (Gimi) Liang (https://github.com/Gimi)"
|
3
|
+
|
4
|
+
COPY . /tmp/fluent-plugin-jq/
|
5
|
+
|
6
|
+
RUN apk update \
|
7
|
+
&& apk add --no-cache jq \
|
8
|
+
&& apk add --no-cache --virtual .build-deps \
|
9
|
+
build-base \
|
10
|
+
git \
|
11
|
+
ruby-dev \
|
12
|
+
&& cd /tmp/fluent-plugin-jq \
|
13
|
+
&& gem build fluent-plugin-jq.gemspec \
|
14
|
+
&& gem install *.gem \
|
15
|
+
&& apk del .build-deps \
|
16
|
+
&& rm -rf /var/cache/apk/* \
|
17
|
+
&& cp /tmp/fluent-plugin-jq/example/fluent.conf /fluentd/etc/ \
|
18
|
+
&& cp /tmp/fluent-plugin-jq/example/statsd.sample / \
|
19
|
+
&& rm -rf /tmp/* /var/tmp/* /usr/lib/ruby/gems/*/cache/*.gem
|
data/example/fluent.conf
ADDED
@@ -0,0 +1,42 @@
|
|
1
|
+
<source>
|
2
|
+
@type dummy
|
3
|
+
tag raw.dummy
|
4
|
+
dummy {"log": "everything goes well", "stream": "stdout"}
|
5
|
+
</source>
|
6
|
+
|
7
|
+
<source>
|
8
|
+
@type tail
|
9
|
+
tag stats
|
10
|
+
path /statsd.sample
|
11
|
+
read_from_head true
|
12
|
+
<parse>
|
13
|
+
@type jq
|
14
|
+
jq 'split(",") | reduce .[] as $item ({}; ($item | split(":")) as $pair | .[$pair[0]] = ($pair[1][:-2] | tonumber))'
|
15
|
+
</parse>
|
16
|
+
</source>
|
17
|
+
|
18
|
+
<match raw.dummy>
|
19
|
+
@type jq
|
20
|
+
jq .record | to_entries
|
21
|
+
remove_tag_prefix raw
|
22
|
+
</match>
|
23
|
+
|
24
|
+
<filter dummy>
|
25
|
+
@type jq_transformer
|
26
|
+
jq .record + {tag, time}
|
27
|
+
</filter>
|
28
|
+
|
29
|
+
<match dummy>
|
30
|
+
@type stdout
|
31
|
+
<format>
|
32
|
+
@type jq
|
33
|
+
jq '"\(.time | todate) [\(.tag)] \(.key) => \(.value)"'
|
34
|
+
</format>
|
35
|
+
</match>
|
36
|
+
|
37
|
+
<match stats>
|
38
|
+
@type stdout
|
39
|
+
<format>
|
40
|
+
@type json
|
41
|
+
</format>
|
42
|
+
</match>
|
@@ -0,0 +1,28 @@
|
|
1
|
+
lib = File.expand_path("../lib", __FILE__)
|
2
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
3
|
+
|
4
|
+
Gem::Specification.new do |spec|
|
5
|
+
spec.name = "test-jq"
|
6
|
+
spec.version = File.read('VERSION')
|
7
|
+
spec.authors = ['Rock']
|
8
|
+
spec.email = ['rbaek@splunk.com']
|
9
|
+
|
10
|
+
spec.summary = %q{Fluentd plugins uses the jq engine.}
|
11
|
+
spec.description = %q{fluent-plungin-jq is a collection of fluentd plugins which uses the jq engine to transform or format fluentd events.}
|
12
|
+
spec.homepage = "https://github.com/rockb1017/fluent-plugin-jq"
|
13
|
+
spec.license = "Apache-2.0"
|
14
|
+
|
15
|
+
spec.files = Dir.glob('*').select { |f| not (File.directory?(f) || f.start_with?('.')) } +
|
16
|
+
Dir.glob('lib/**/**.rb') +
|
17
|
+
Dir.glob('example/**/**')
|
18
|
+
spec.test_files = Dir.glob('test/**/**.rb')
|
19
|
+
spec.require_paths = ["lib"]
|
20
|
+
|
21
|
+
spec.add_development_dependency "bundler", "~> 1.14"
|
22
|
+
spec.add_development_dependency "rake", "~> 12.0"
|
23
|
+
spec.add_development_dependency "test-unit", "~> 3.0"
|
24
|
+
spec.add_development_dependency "coveralls", "~> 0.8"
|
25
|
+
|
26
|
+
spec.add_runtime_dependency "fluentd", [">= 0.14.10", "< 2"]
|
27
|
+
spec.add_runtime_dependency "multi_json", "~> 1.13"
|
28
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Copyright 2018- Zhimin (Gimi) Liang (https://github.com/Gimi)
|
4
|
+
#
|
5
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
6
|
+
# you may not use this file except in compliance with the License.
|
7
|
+
# You may obtain a copy of the License at
|
8
|
+
#
|
9
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
10
|
+
#
|
11
|
+
# Unless required by applicable law or agreed to in writing, software
|
12
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
13
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
14
|
+
# See the License for the specific language governing permissions and
|
15
|
+
# limitations under the License.
|
16
|
+
|
17
|
+
require "fluent/plugin/filter"
|
18
|
+
require "fluent/plugin/jq_mixin"
|
19
|
+
|
20
|
+
module Fluent
|
21
|
+
module Plugin
|
22
|
+
class JqTransformerFilter < Fluent::Plugin::Filter
|
23
|
+
Fluent::Plugin.register_filter("jq_transformer", self)
|
24
|
+
|
25
|
+
include JqMixin
|
26
|
+
|
27
|
+
config_set_desc :jq, 'The jq filter used to transform the input. The result of the filter should return an object.'
|
28
|
+
|
29
|
+
def filter(tag, time, record)
|
30
|
+
new_record = jq_transform tag: tag, time: time, record: record
|
31
|
+
return new_record if new_record.is_a?(Hash)
|
32
|
+
|
33
|
+
log.error "jq filter #{@jq} did not return a hash, skip this record."
|
34
|
+
nil
|
35
|
+
rescue JqError
|
36
|
+
log.error "Filter failed with #{@jq}#{log.on_debug { ' on ' + MultiJson.dump(tag: tag, time: time, record: record) }}, error: #{$!.message}"
|
37
|
+
nil
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Copyright 2018- Zhimin (Gimi) Liang (https://github.com/Gimi)
|
4
|
+
#
|
5
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
6
|
+
# you may not use this file except in compliance with the License.
|
7
|
+
# You may obtain a copy of the License at
|
8
|
+
#
|
9
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
10
|
+
#
|
11
|
+
# Unless required by applicable law or agreed to in writing, software
|
12
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
13
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
14
|
+
# See the License for the specific language governing permissions and
|
15
|
+
# limitations under the License.
|
16
|
+
|
17
|
+
require "fluent/plugin/formatter"
|
18
|
+
require "fluent/plugin/jq_mixin"
|
19
|
+
|
20
|
+
module Fluent
|
21
|
+
module Plugin
|
22
|
+
class JqFormatter < Fluent::Plugin::Formatter
|
23
|
+
Fluent::Plugin.register_formatter("jq", self)
|
24
|
+
|
25
|
+
include JqMixin
|
26
|
+
|
27
|
+
config_set_desc :jq, 'The jq filter used to format income events. If the result returned from the filter is not a string, it will be encoded as a JSON string.'
|
28
|
+
|
29
|
+
desc 'Defines the behavior on error happens when formatting an event. "skip" will skip the event; "ignore" will ignore the error and return the JSON representation of the original event; "raise_error" will raise a RuntimeError.'
|
30
|
+
config_param :on_error, :enum, list: [:skip, :ignore, :raise_error], default: :ignore
|
31
|
+
|
32
|
+
def initialize
|
33
|
+
super
|
34
|
+
end
|
35
|
+
|
36
|
+
def format(tag, time, record)
|
37
|
+
item = jq_transform record
|
38
|
+
if item.instance_of?(String)
|
39
|
+
item
|
40
|
+
else
|
41
|
+
MultiJson.dump item
|
42
|
+
end
|
43
|
+
rescue JqError
|
44
|
+
msg = "Format failed with #{@jq}#{log.on_debug { ' on ' + MultiJson.dump(record) }}, error: #{$!.message}"
|
45
|
+
log.error msg
|
46
|
+
case @on_error
|
47
|
+
when :skip
|
48
|
+
return ''
|
49
|
+
when :ignore
|
50
|
+
return MultiJson.dump(record)
|
51
|
+
when :raise_error
|
52
|
+
raise msg
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
require 'shellwords'
|
2
|
+
require 'multi_json'
|
3
|
+
|
4
|
+
module JqMixin
|
5
|
+
JqError = Class.new(RuntimeError)
|
6
|
+
|
7
|
+
def self.included(plugin)
|
8
|
+
plugin.config_param :jq, :string
|
9
|
+
end
|
10
|
+
|
11
|
+
def configure(conf)
|
12
|
+
super
|
13
|
+
p = start_process(null_input: true)
|
14
|
+
err = p.read
|
15
|
+
raise Fluent::ConfigError, "Could not parse jq filter: #{@jq}, error: #{err}" if err =~ /compile error/m
|
16
|
+
rescue
|
17
|
+
raise Fluent::ConfigError, "Could not parse jq filter: #{@jq}, error: #{$!.message}"
|
18
|
+
ensure
|
19
|
+
p.close if p # if `super` fails, `p` will be `nil`
|
20
|
+
end
|
21
|
+
|
22
|
+
def start
|
23
|
+
super
|
24
|
+
@jq_process = start_process
|
25
|
+
@lock = Thread::Mutex.new
|
26
|
+
end
|
27
|
+
|
28
|
+
def shutdown
|
29
|
+
@jq_process.close rescue nil
|
30
|
+
super
|
31
|
+
end
|
32
|
+
|
33
|
+
def start_process(filter: @jq, null_input: false)
|
34
|
+
IO.popen(%Q"jq #{'-n' if null_input} --unbuffered -c '#{filter}' 2>&1", 'r+')
|
35
|
+
end
|
36
|
+
|
37
|
+
def jq_transform(object)
|
38
|
+
result = @lock.synchronize do
|
39
|
+
@jq_process.puts MultiJson.dump(object)
|
40
|
+
@jq_process.gets
|
41
|
+
end
|
42
|
+
MultiJson.load result
|
43
|
+
rescue MultiJson::ParseError
|
44
|
+
raise JqError.new(result)
|
45
|
+
rescue Errno::EPIPE
|
46
|
+
@jq_process.close
|
47
|
+
@jq_process = start_process
|
48
|
+
retry
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Copyright 2018- Zhimin (Gimi) Liang (https://github.com/Gimi)
|
4
|
+
#
|
5
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
6
|
+
# you may not use this file except in compliance with the License.
|
7
|
+
# You may obtain a copy of the License at
|
8
|
+
#
|
9
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
10
|
+
#
|
11
|
+
# Unless required by applicable law or agreed to in writing, software
|
12
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
13
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
14
|
+
# See the License for the specific language governing permissions and
|
15
|
+
# limitations under the License.
|
16
|
+
|
17
|
+
require 'fluent/plugin/output'
|
18
|
+
require 'fluent/plugin/jq_mixin'
|
19
|
+
|
20
|
+
module Fluent::Plugin
|
21
|
+
class JqOutput < Output
|
22
|
+
Fluent::Plugin.register_output('jq', self)
|
23
|
+
helpers :event_emitter
|
24
|
+
|
25
|
+
include JqMixin
|
26
|
+
|
27
|
+
config_set_desc :jq, 'The jq filter used to transform the input. If the filter returns an array, each object in the array will be a new record.'
|
28
|
+
|
29
|
+
desc 'The prefix to be removed from the input tag when outputting a new record.'
|
30
|
+
config_param :remove_tag_prefix, :string, default: ''
|
31
|
+
|
32
|
+
def multi_workers_ready?
|
33
|
+
true
|
34
|
+
end
|
35
|
+
|
36
|
+
def process(tag, es)
|
37
|
+
new_es = Fluent::MultiEventStream.new
|
38
|
+
es.each do |time, record|
|
39
|
+
begin
|
40
|
+
new_records = jq_transform tag: tag, time: time, record: record
|
41
|
+
new_records = [new_records] unless new_records.is_a?(Array)
|
42
|
+
new_records.each { |new_record| new_es.add time, new_record }
|
43
|
+
rescue JqError
|
44
|
+
log.error "Process failed with #{@jq}#{log.on_debug {' on ' + MultiJson.dump(record)}}, error: #{$!.message}"
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
new_tag = tag.sub(/^#{Regexp.escape(@remove_tag_prefix)}\./, '')
|
49
|
+
router.emit_stream(new_tag, new_es)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Copyright 2018- Zhimin (Gimi) Liang (https://github.com/Gimi)
|
4
|
+
#
|
5
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
6
|
+
# you may not use this file except in compliance with the License.
|
7
|
+
# You may obtain a copy of the License at
|
8
|
+
#
|
9
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
10
|
+
#
|
11
|
+
# Unless required by applicable law or agreed to in writing, software
|
12
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
13
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
14
|
+
# See the License for the specific language governing permissions and
|
15
|
+
# limitations under the License.
|
16
|
+
|
17
|
+
require "fluent/plugin/parser"
|
18
|
+
require 'fluent/plugin/jq_mixin'
|
19
|
+
|
20
|
+
module Fluent
|
21
|
+
module Plugin
|
22
|
+
class JqParser < Fluent::Plugin::Parser
|
23
|
+
Fluent::Plugin.register_parser("jq", self)
|
24
|
+
|
25
|
+
include JqMixin
|
26
|
+
|
27
|
+
config_set_desc :jq, 'The jq filter used to format the input. The result of the filter must return an object.'
|
28
|
+
|
29
|
+
def parse(text)
|
30
|
+
record = jq_transform text
|
31
|
+
if record.is_a?(Hash)
|
32
|
+
yield parse_time(record), record
|
33
|
+
else
|
34
|
+
log.error "jq filter #{@jq} did not return a hash, skip this record."
|
35
|
+
end
|
36
|
+
rescue JqError
|
37
|
+
log.error "Parse failed with #{@jq}#{log.on_debug {' on ' + text}}, error: #{$!.message}"
|
38
|
+
nil
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
data/run_ci.sh
ADDED
data/test/helper.rb
ADDED
@@ -0,0 +1,10 @@
|
|
1
|
+
$LOAD_PATH.unshift(File.expand_path("../../", __FILE__))
|
2
|
+
require "test-unit"
|
3
|
+
require "fluent/test"
|
4
|
+
require "fluent/test/helpers"
|
5
|
+
require 'coveralls'
|
6
|
+
|
7
|
+
Coveralls.wear!
|
8
|
+
|
9
|
+
Test::Unit::TestCase.include(Fluent::Test::Helpers)
|
10
|
+
Test::Unit::TestCase.extend(Fluent::Test::Helpers)
|
@@ -0,0 +1,74 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "helper"
|
4
|
+
require "fluent/test/driver/filter"
|
5
|
+
require "fluent/plugin/filter_jq_transformer"
|
6
|
+
|
7
|
+
class JqTransformerFilterTest < Test::Unit::TestCase
|
8
|
+
setup do
|
9
|
+
Fluent::Test.setup
|
10
|
+
end
|
11
|
+
|
12
|
+
teardown do
|
13
|
+
@driver.instance.shutdown if @driver
|
14
|
+
end
|
15
|
+
|
16
|
+
test "it should require jq" do
|
17
|
+
assert_raise(Fluent::ConfigError) { create_driver '' }
|
18
|
+
end
|
19
|
+
|
20
|
+
test "it should raise error on invalid jq program" do
|
21
|
+
e = assert_raise(Fluent::ConfigError) { create_driver 'jq blah' }
|
22
|
+
assert_match(/compile error/, e.message)
|
23
|
+
end
|
24
|
+
|
25
|
+
test "it should work on tag" do
|
26
|
+
d = create_driver 'jq "{tag}"'
|
27
|
+
record = {"log" => "this is a log", "source" => "stdout"}
|
28
|
+
record = d.instance.filter("some.tag", event_time, record)
|
29
|
+
assert_equal record["tag"], "some.tag"
|
30
|
+
end
|
31
|
+
|
32
|
+
test "it should work on time" do
|
33
|
+
d = create_driver 'jq "{time: .time | gmtime }"'
|
34
|
+
now = event_time
|
35
|
+
record = {"log" => "this is a log", "source" => "stdout"}
|
36
|
+
record = d.instance.filter("some.tag", now, record)
|
37
|
+
assert_equal record["time"][0..5],
|
38
|
+
Time.at(now).utc.to_a[0..5].reverse.tap { |a| a[1] = a[1] - 1 }
|
39
|
+
end
|
40
|
+
|
41
|
+
test "it should work on record" do
|
42
|
+
d = create_driver 'jq "{message: .record.log, stream: .record.source}"'
|
43
|
+
record = {"log" => "this is a log", "source" => "stdout"}
|
44
|
+
record = d.instance.filter("some.tag", event_time, record)
|
45
|
+
assert_equal record["message"], "this is a log"
|
46
|
+
assert_equal record["stream"], "stdout"
|
47
|
+
end
|
48
|
+
|
49
|
+
test "it should skip if it does not return a hash" do
|
50
|
+
record = {"log" => "this is a log", "source" => "stdout"}
|
51
|
+
|
52
|
+
d = create_driver 'jq ".tag"'
|
53
|
+
assert_nil d.instance.filter("some.tag", event_time, record)
|
54
|
+
|
55
|
+
d = create_driver 'jq ".time"'
|
56
|
+
assert_nil d.instance.filter("some.tag", event_time, record)
|
57
|
+
|
58
|
+
d = create_driver 'jq "[.time]"'
|
59
|
+
assert_nil d.instance.filter("some.tag", event_time, record)
|
60
|
+
end
|
61
|
+
|
62
|
+
test "it should only return one object" do
|
63
|
+
d = create_driver 'jq ".record, {tag, time}"'
|
64
|
+
event = {"log" => "this is a log", "source" => "stdout"}
|
65
|
+
record = d.instance.filter("some.tag", event_time, event)
|
66
|
+
assert_equal event, record
|
67
|
+
end
|
68
|
+
|
69
|
+
private
|
70
|
+
|
71
|
+
def create_driver(conf)
|
72
|
+
@driver = Fluent::Test::Driver::Filter.new(Fluent::Plugin::JqTransformerFilter).configure(conf).tap { |d| d.instance.start }
|
73
|
+
end
|
74
|
+
end
|