fluent-plugin-fields-parser 0.1.0
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/.gitignore +34 -0
- data/README.md +119 -0
- data/Rakefile +14 -0
- data/VERSION +1 -0
- data/fluent-plugin-tokenizer.gemspec +22 -0
- data/lib/fluent/plugin/out_fields_parser.rb +54 -0
- data/test/out_fields_parser.rb +234 -0
- metadata +85 -0
data/.gitignore
ADDED
@@ -0,0 +1,34 @@
|
|
1
|
+
*.gem
|
2
|
+
*.rbc
|
3
|
+
/.config
|
4
|
+
/coverage/
|
5
|
+
/InstalledFiles
|
6
|
+
/pkg/
|
7
|
+
/spec/reports/
|
8
|
+
/test/tmp/
|
9
|
+
/test/version_tmp/
|
10
|
+
/tmp/
|
11
|
+
|
12
|
+
## Specific to RubyMotion:
|
13
|
+
.dat*
|
14
|
+
.repl_history
|
15
|
+
build/
|
16
|
+
|
17
|
+
## Documentation cache and generated files:
|
18
|
+
/.yardoc/
|
19
|
+
/_yardoc/
|
20
|
+
/doc/
|
21
|
+
/rdoc/
|
22
|
+
|
23
|
+
## Environment normalisation:
|
24
|
+
/.bundle/
|
25
|
+
/lib/bundler/man/
|
26
|
+
|
27
|
+
# for a library or gem, you might want to ignore these files since the code is
|
28
|
+
# intended to run in multiple environments; otherwise, check them in:
|
29
|
+
# Gemfile.lock
|
30
|
+
# .ruby-version
|
31
|
+
# .ruby-gemset
|
32
|
+
|
33
|
+
# unless supporting rvm < 1.11.0 or doing something fancy, ignore this:
|
34
|
+
.rvmrc
|
data/README.md
ADDED
@@ -0,0 +1,119 @@
|
|
1
|
+
fluent-plugin-fields-parser
|
2
|
+
===========================
|
3
|
+
|
4
|
+
Fluent output filter plugin for parsing key/value fields in records
|
5
|
+
based on <key>=<value> pattern.
|
6
|
+
|
7
|
+
## Installation
|
8
|
+
|
9
|
+
Use RubyGems:
|
10
|
+
|
11
|
+
gem install fluent-plugin-fields-parser
|
12
|
+
|
13
|
+
## Configuration
|
14
|
+
|
15
|
+
<match pattern>
|
16
|
+
type fields_parser
|
17
|
+
|
18
|
+
remove_tag_prefix raw
|
19
|
+
add_tag_prefix parsed
|
20
|
+
</match>
|
21
|
+
|
22
|
+
If following record is passed:
|
23
|
+
|
24
|
+
```
|
25
|
+
{"message": "Audit log user=Johny action='add-user' result=success" }
|
26
|
+
```
|
27
|
+
|
28
|
+
then you got new record:
|
29
|
+
|
30
|
+
```
|
31
|
+
{
|
32
|
+
"message": "Audit log username=Johny action='add-user' result=success",
|
33
|
+
"user": "Johny",
|
34
|
+
"action": "add-user",
|
35
|
+
"result": "success"
|
36
|
+
}
|
37
|
+
```
|
38
|
+
|
39
|
+
### Parameter parse_key
|
40
|
+
|
41
|
+
For configuration
|
42
|
+
|
43
|
+
<match pattern>
|
44
|
+
type fields_parser
|
45
|
+
|
46
|
+
parse_key log_message
|
47
|
+
</match>
|
48
|
+
|
49
|
+
it parses key "log_message" instead of default key `message`.
|
50
|
+
|
51
|
+
### Parameter fields_key
|
52
|
+
|
53
|
+
Configuration
|
54
|
+
|
55
|
+
<match pattern>
|
56
|
+
type fields_parser
|
57
|
+
|
58
|
+
parse_key log_message
|
59
|
+
fields_key fields
|
60
|
+
</match>
|
61
|
+
|
62
|
+
For input like:
|
63
|
+
|
64
|
+
```
|
65
|
+
{
|
66
|
+
"log_message": "Audit log username=Johny action='add-user' result=success",
|
67
|
+
}
|
68
|
+
```
|
69
|
+
|
70
|
+
it adds parsed fields into defined key.
|
71
|
+
|
72
|
+
```
|
73
|
+
{
|
74
|
+
"log_message": "Audit log username=Johny action='add-user' result=success",
|
75
|
+
"fields": {"user": "Johny", "action": "add-user", "result": "success"}
|
76
|
+
}
|
77
|
+
```
|
78
|
+
|
79
|
+
(It adds new keys into top-level record by default.)
|
80
|
+
|
81
|
+
### Parameter pattern
|
82
|
+
|
83
|
+
You can define custom pattern (regexp) for seaching keys/values.
|
84
|
+
|
85
|
+
Configuration
|
86
|
+
|
87
|
+
<match pattern>
|
88
|
+
type fields_parser
|
89
|
+
|
90
|
+
pattern (\w+):(\d+)
|
91
|
+
</match>
|
92
|
+
|
93
|
+
For input like:
|
94
|
+
```
|
95
|
+
{ "message": "data black:54 white=55 red=10"}
|
96
|
+
```
|
97
|
+
|
98
|
+
it returns:
|
99
|
+
|
100
|
+
```
|
101
|
+
{ "message": "data black:54 white=55 red=10",
|
102
|
+
"black": "54", "white": "55", "red": "10"
|
103
|
+
}
|
104
|
+
```
|
105
|
+
|
106
|
+
### Tag prefix
|
107
|
+
|
108
|
+
You cat add and/or remove tag prefix using Configuration parameters
|
109
|
+
|
110
|
+
<match pattern>
|
111
|
+
type fields_parser
|
112
|
+
|
113
|
+
remove_tag_prefix raw
|
114
|
+
add_tag_prefix parsed
|
115
|
+
</match>
|
116
|
+
|
117
|
+
It it matched tag "raw.some.record", then it emits tag "parsed.some.record".
|
118
|
+
|
119
|
+
|
data/Rakefile
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
#!/usr/bin/env rake
|
2
|
+
|
3
|
+
require "bundler/gem_tasks"
|
4
|
+
Bundler::GemHelper.install_tasks
|
5
|
+
|
6
|
+
require 'rake/testtask'
|
7
|
+
|
8
|
+
Rake::TestTask.new(:test) do |test|
|
9
|
+
test.libs << 'lib' << 'test'
|
10
|
+
test.test_files = FileList['test/*.rb']
|
11
|
+
test.verbose = true
|
12
|
+
end
|
13
|
+
|
14
|
+
task :default => [:build]
|
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
0.1.0
|
@@ -0,0 +1,22 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
$:.push File.expand_path('../lib', __FILE__)
|
3
|
+
|
4
|
+
Gem::Specification.new do |gem|
|
5
|
+
gem.name = "fluent-plugin-fields-parser"
|
6
|
+
gem.description = "Fluent output filter plugin for parsing key/value fields in records"
|
7
|
+
gem.homepage = "https://github.com/tomas-zemres/fluent-plugin-fields-parser"
|
8
|
+
gem.summary = gem.description
|
9
|
+
gem.version = File.read("VERSION").strip
|
10
|
+
gem.authors = ["Tomas Pokorny"]
|
11
|
+
gem.email = ["tomas.zemres@gmail.com"]
|
12
|
+
gem.has_rdoc = false
|
13
|
+
gem.license = 'MIT'
|
14
|
+
|
15
|
+
gem.files = `git ls-files`.split("\n")
|
16
|
+
gem.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
17
|
+
gem.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
18
|
+
gem.require_paths = ['lib']
|
19
|
+
|
20
|
+
gem.add_dependency "fluentd"
|
21
|
+
gem.add_development_dependency "rake"
|
22
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
module Fluent
|
2
|
+
class OutputFieldsParser < Fluent::Output
|
3
|
+
Fluent::Plugin.register_output('fields_parser', self)
|
4
|
+
|
5
|
+
config_param :remove_tag_prefix, :string, :default => nil
|
6
|
+
config_param :add_tag_prefix, :string, :default => nil
|
7
|
+
config_param :parse_key, :string, :default => 'message'
|
8
|
+
config_param :fields_key, :string, :default => nil
|
9
|
+
config_param :pattern, :string,
|
10
|
+
:default => %{([a-zA-Z_]\\w*)=((['"]).*?(\\3)|[\\w.@$%/+-]*)}
|
11
|
+
|
12
|
+
def compiled_pattern
|
13
|
+
@compiled_pattern ||= Regexp.new(pattern)
|
14
|
+
end
|
15
|
+
|
16
|
+
def emit(tag, es, chain)
|
17
|
+
tag = update_tag(tag)
|
18
|
+
es.each { |time, record|
|
19
|
+
Engine.emit(tag, time, parse_fields(record))
|
20
|
+
}
|
21
|
+
chain.next
|
22
|
+
end
|
23
|
+
|
24
|
+
def update_tag(tag)
|
25
|
+
if remove_tag_prefix
|
26
|
+
if remove_tag_prefix == tag
|
27
|
+
tag = ''
|
28
|
+
elsif tag.to_s.start_with?(remove_tag_prefix+'.')
|
29
|
+
tag = tag[remove_tag_prefix.length+1 .. -1]
|
30
|
+
end
|
31
|
+
end
|
32
|
+
if add_tag_prefix
|
33
|
+
tag = tag && tag.length > 0 ? "#{add_tag_prefix}.#{tag}" : add_tag_prefix
|
34
|
+
end
|
35
|
+
return tag
|
36
|
+
end
|
37
|
+
|
38
|
+
def parse_fields(record)
|
39
|
+
source = record[parse_key].to_s
|
40
|
+
target = fields_key ? (record[fields_key] ||= {}) : record
|
41
|
+
|
42
|
+
source.scan(compiled_pattern) do |match|
|
43
|
+
(key, value, begining_quote, ending_quote) = match
|
44
|
+
next if key.nil?
|
45
|
+
next if target.has_key?(key)
|
46
|
+
value = value.to_s
|
47
|
+
from_pos = begining_quote.to_s.length
|
48
|
+
to_pos = value.length - ending_quote.to_s.length - 1
|
49
|
+
target[key] = value[from_pos..to_pos]
|
50
|
+
end
|
51
|
+
return record
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,234 @@
|
|
1
|
+
require 'fluent/test'
|
2
|
+
require 'fluent/plugin/out_fields_parser'
|
3
|
+
|
4
|
+
class FieldsParserOutputTest < Test::Unit::TestCase
|
5
|
+
def setup
|
6
|
+
Fluent::Test.setup
|
7
|
+
end
|
8
|
+
|
9
|
+
def create_driver(conf='', tag='orig.test.tag')
|
10
|
+
Fluent::Test::OutputTestDriver.new(Fluent::OutputFieldsParser, tag).configure(conf)
|
11
|
+
end
|
12
|
+
|
13
|
+
def test_config_defaults
|
14
|
+
d = create_driver()
|
15
|
+
|
16
|
+
orig_message = %{parse this num=-56.7 tok=abc%25 null=}
|
17
|
+
d.run do
|
18
|
+
d.emit({
|
19
|
+
'message' => orig_message,
|
20
|
+
'other_key' => %{ test2 a=b },
|
21
|
+
})
|
22
|
+
end
|
23
|
+
|
24
|
+
emits = d.emits
|
25
|
+
assert_equal 1, emits.size
|
26
|
+
assert_equal "orig.test.tag", emits[0][0]
|
27
|
+
assert_equal(
|
28
|
+
{
|
29
|
+
'message' => orig_message,
|
30
|
+
'other_key' => %{ test2 a=b },
|
31
|
+
'num' => '-56.7',
|
32
|
+
'tok' => 'abc%25',
|
33
|
+
'null' => '',
|
34
|
+
},
|
35
|
+
emits[0][2]
|
36
|
+
)
|
37
|
+
end
|
38
|
+
|
39
|
+
def test_quoted_values
|
40
|
+
d = create_driver()
|
41
|
+
|
42
|
+
orig_message = %{blax dq="asd ' asd +3" sq='as " s " 4' s=yu 6}
|
43
|
+
d.run do
|
44
|
+
d.emit({
|
45
|
+
'message' => orig_message,
|
46
|
+
})
|
47
|
+
end
|
48
|
+
|
49
|
+
emits = d.emits
|
50
|
+
assert_equal 1, emits.size
|
51
|
+
assert_equal "orig.test.tag", emits[0][0]
|
52
|
+
assert_equal(
|
53
|
+
{
|
54
|
+
'message' => orig_message,
|
55
|
+
'dq' => "asd ' asd +3",
|
56
|
+
'sq' => 'as " s " 4',
|
57
|
+
's' => 'yu'
|
58
|
+
},
|
59
|
+
emits[0][2]
|
60
|
+
)
|
61
|
+
end
|
62
|
+
|
63
|
+
def test_parsed_key_is_missing
|
64
|
+
d = create_driver()
|
65
|
+
|
66
|
+
d.run do
|
67
|
+
d.emit({})
|
68
|
+
end
|
69
|
+
|
70
|
+
emits = d.emits
|
71
|
+
assert_equal 1, emits.size
|
72
|
+
assert_equal "orig.test.tag", emits[0][0]
|
73
|
+
assert_equal(
|
74
|
+
{},
|
75
|
+
emits[0][2]
|
76
|
+
)
|
77
|
+
end
|
78
|
+
|
79
|
+
def test_existing_keys_are_not_overriden
|
80
|
+
d = create_driver()
|
81
|
+
|
82
|
+
orig_message = %{mock a=77 message=blax a=999 e=5}
|
83
|
+
d.run do
|
84
|
+
d.emit({'message' => orig_message, 'e' => nil })
|
85
|
+
end
|
86
|
+
|
87
|
+
emits = d.emits
|
88
|
+
assert_equal 1, emits.size
|
89
|
+
assert_equal "orig.test.tag", emits[0][0]
|
90
|
+
assert_equal(
|
91
|
+
{
|
92
|
+
'message' => orig_message,
|
93
|
+
'a' => '77',
|
94
|
+
'e' => nil,
|
95
|
+
},
|
96
|
+
emits[0][2]
|
97
|
+
)
|
98
|
+
end
|
99
|
+
|
100
|
+
def test_tag_prefixes
|
101
|
+
d = create_driver(%{
|
102
|
+
remove_tag_prefix orig
|
103
|
+
add_tag_prefix new
|
104
|
+
})
|
105
|
+
|
106
|
+
d.run do
|
107
|
+
d.emit({ message => 'abc' })
|
108
|
+
end
|
109
|
+
|
110
|
+
emits = d.emits
|
111
|
+
assert_equal 1, emits.size
|
112
|
+
assert_equal "new.test.tag", emits[0][0]
|
113
|
+
|
114
|
+
d = create_driver(%{
|
115
|
+
remove_tag_prefix orig
|
116
|
+
add_tag_prefix new
|
117
|
+
}, tag=nil)
|
118
|
+
|
119
|
+
d.run do
|
120
|
+
d.emit({ message => 'abc' })
|
121
|
+
end
|
122
|
+
|
123
|
+
emits = d.emits
|
124
|
+
assert_equal 1, emits.size
|
125
|
+
assert_equal "new", emits[0][0]
|
126
|
+
|
127
|
+
d = create_driver(%{
|
128
|
+
remove_tag_prefix orig
|
129
|
+
add_tag_prefix new
|
130
|
+
}, tag='original')
|
131
|
+
|
132
|
+
d.run do
|
133
|
+
d.emit({ message => 'abc' })
|
134
|
+
end
|
135
|
+
|
136
|
+
emits = d.emits
|
137
|
+
assert_equal 1, emits.size
|
138
|
+
assert_equal "new.original", emits[0][0]
|
139
|
+
|
140
|
+
d = create_driver(%{
|
141
|
+
remove_tag_prefix orig
|
142
|
+
add_tag_prefix new
|
143
|
+
}, tag='orig')
|
144
|
+
|
145
|
+
d.run do
|
146
|
+
d.emit({ message => 'abc' })
|
147
|
+
end
|
148
|
+
|
149
|
+
emits = d.emits
|
150
|
+
assert_equal 1, emits.size
|
151
|
+
assert_equal "new", emits[0][0]
|
152
|
+
end
|
153
|
+
|
154
|
+
def test_parse_key
|
155
|
+
d = create_driver('parse_key custom_key')
|
156
|
+
|
157
|
+
d.run do
|
158
|
+
d.emit({
|
159
|
+
'message' => %{ test2 c=d },
|
160
|
+
'custom_key' => %{ test2 a=b },
|
161
|
+
})
|
162
|
+
d.emit({})
|
163
|
+
end
|
164
|
+
|
165
|
+
emits = d.emits
|
166
|
+
assert_equal 2, emits.size
|
167
|
+
assert_equal "orig.test.tag", emits[0][0]
|
168
|
+
assert_equal(
|
169
|
+
{
|
170
|
+
'message' => %{ test2 c=d },
|
171
|
+
'custom_key' => %{ test2 a=b },
|
172
|
+
'a' => 'b'
|
173
|
+
},
|
174
|
+
emits[0][2]
|
175
|
+
)
|
176
|
+
assert_equal(
|
177
|
+
{
|
178
|
+
},
|
179
|
+
emits[1][2]
|
180
|
+
)
|
181
|
+
end
|
182
|
+
|
183
|
+
def test_fields_key
|
184
|
+
d = create_driver("fields_key output-key")
|
185
|
+
|
186
|
+
orig_message = %{parse this num=-56.7 tok=abc%25 message=a+b}
|
187
|
+
d.run do
|
188
|
+
d.emit({'message' => orig_message})
|
189
|
+
end
|
190
|
+
|
191
|
+
emits = d.emits
|
192
|
+
assert_equal 1, emits.size
|
193
|
+
assert_equal "orig.test.tag", emits[0][0]
|
194
|
+
assert_equal(
|
195
|
+
{
|
196
|
+
'message' => orig_message,
|
197
|
+
'output-key' => {
|
198
|
+
'num' => '-56.7',
|
199
|
+
'tok' => 'abc%25',
|
200
|
+
'message' => 'a+b',
|
201
|
+
}
|
202
|
+
},
|
203
|
+
emits[0][2]
|
204
|
+
)
|
205
|
+
end
|
206
|
+
|
207
|
+
def test_custom_pattern
|
208
|
+
d = create_driver("pattern (\\w+):(\\d+)")
|
209
|
+
|
210
|
+
orig_message = %{parse this a:44 b:ignore-this h=7 bbb:999}
|
211
|
+
d.run do
|
212
|
+
d.emit({'message' => orig_message})
|
213
|
+
d.emit({'message' => 'a'})
|
214
|
+
end
|
215
|
+
|
216
|
+
emits = d.emits
|
217
|
+
assert_equal 2, emits.size
|
218
|
+
assert_equal "orig.test.tag", emits[0][0]
|
219
|
+
assert_equal(
|
220
|
+
{
|
221
|
+
'message' => orig_message,
|
222
|
+
'a' => '44',
|
223
|
+
'bbb' => '999',
|
224
|
+
},
|
225
|
+
emits[0][2]
|
226
|
+
)
|
227
|
+
assert_equal(
|
228
|
+
{
|
229
|
+
'message' => 'a',
|
230
|
+
},
|
231
|
+
emits[1][2]
|
232
|
+
)
|
233
|
+
end
|
234
|
+
end
|
metadata
ADDED
@@ -0,0 +1,85 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: fluent-plugin-fields-parser
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Tomas Pokorny
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2014-07-07 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: fluentd
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ! '>='
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: '0'
|
22
|
+
type: :runtime
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ! '>='
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: '0'
|
30
|
+
- !ruby/object:Gem::Dependency
|
31
|
+
name: rake
|
32
|
+
requirement: !ruby/object:Gem::Requirement
|
33
|
+
none: false
|
34
|
+
requirements:
|
35
|
+
- - ! '>='
|
36
|
+
- !ruby/object:Gem::Version
|
37
|
+
version: '0'
|
38
|
+
type: :development
|
39
|
+
prerelease: false
|
40
|
+
version_requirements: !ruby/object:Gem::Requirement
|
41
|
+
none: false
|
42
|
+
requirements:
|
43
|
+
- - ! '>='
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
version: '0'
|
46
|
+
description: Fluent output filter plugin for parsing key/value fields in records
|
47
|
+
email:
|
48
|
+
- tomas.zemres@gmail.com
|
49
|
+
executables: []
|
50
|
+
extensions: []
|
51
|
+
extra_rdoc_files: []
|
52
|
+
files:
|
53
|
+
- .gitignore
|
54
|
+
- README.md
|
55
|
+
- Rakefile
|
56
|
+
- VERSION
|
57
|
+
- fluent-plugin-tokenizer.gemspec
|
58
|
+
- lib/fluent/plugin/out_fields_parser.rb
|
59
|
+
- test/out_fields_parser.rb
|
60
|
+
homepage: https://github.com/tomas-zemres/fluent-plugin-fields-parser
|
61
|
+
licenses:
|
62
|
+
- MIT
|
63
|
+
post_install_message:
|
64
|
+
rdoc_options: []
|
65
|
+
require_paths:
|
66
|
+
- lib
|
67
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
68
|
+
none: false
|
69
|
+
requirements:
|
70
|
+
- - ! '>='
|
71
|
+
- !ruby/object:Gem::Version
|
72
|
+
version: '0'
|
73
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
74
|
+
none: false
|
75
|
+
requirements:
|
76
|
+
- - ! '>='
|
77
|
+
- !ruby/object:Gem::Version
|
78
|
+
version: '0'
|
79
|
+
requirements: []
|
80
|
+
rubyforge_project:
|
81
|
+
rubygems_version: 1.8.23
|
82
|
+
signing_key:
|
83
|
+
specification_version: 3
|
84
|
+
summary: Fluent output filter plugin for parsing key/value fields in records
|
85
|
+
test_files: []
|