test-jq 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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