test-jq 0.5.1

Sign up to get free protection for your applications and to get access to all the features.
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
@@ -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
@@ -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,4 @@
1
+ cpu.usage:10|g,cpu.free:90|g
2
+ memory.usage:100|g,memory.rss:80|g
3
+ network.rx:12.34|g,network.tx:56.78|g
4
+ login.success:100|c,login.failed:20|c
@@ -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
@@ -0,0 +1,5 @@
1
+ #!/bin/sh
2
+
3
+ apk add --no-cache build-base jq-dev \
4
+ && bundle \
5
+ && rake
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