├── .gitignore
├── Gemfile
├── LICENSE.txt
├── README.md
├── Rakefile
├── fluent-plugin-postgres.gemspec
├── lib
└── fluent
│ └── plugin
│ └── out_postgres.rb
└── test
├── helper.rb
└── plugin
└── test_out_postgres.rb
/.gitignore:
--------------------------------------------------------------------------------
1 | *.gem
2 | *.rbc
3 | .bundle
4 | .config
5 | .yardoc
6 | Gemfile.lock
7 | InstalledFiles
8 | _yardoc
9 | coverage
10 | doc/
11 | lib/bundler/man
12 | pkg
13 | rdoc
14 | spec/reports
15 | test/tmp
16 | test/version_tmp
17 | tmp
18 | test.conf
19 |
--------------------------------------------------------------------------------
/Gemfile:
--------------------------------------------------------------------------------
1 | source :rubygems
2 |
3 | gemspec
4 |
--------------------------------------------------------------------------------
/LICENSE.txt:
--------------------------------------------------------------------------------
1 | Copyright 2013 Uken Games
2 |
3 | Licensed under the Apache License, Version 2.0 (the "License");
4 | you may not use this file except in compliance with the License.
5 | You may obtain a copy of the License at
6 |
7 | http://www.apache.org/licenses/LICENSE-2.0
8 |
9 | Unless required by applicable law or agreed to in writing, software
10 | distributed under the License is distributed on an "AS IS" BASIS,
11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | See the License for the specific language governing permissions and
13 | limitations under the License.
14 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # fluent-plugin-postgres, a plugin for [Fluentd](http://fluentd.org)
2 |
3 | ## Installation:
4 |
5 | - Prereq: Install postgresql headers: `apt-get install libpq-dev`
6 | - Install the gem:
7 | - `gem install fluent-plugin-postgres` or
8 | - `/usr/lib/fluent/ruby/bin/fluent-gem install fluent-plugin-postgres`
9 |
10 | ## Changes from mysql:
11 |
12 | - We currently don't support json format
13 | - You need to specify a SQL query
14 | - Placeholders are numbered (yeah, I know).
15 |
16 | Other than that, just bear in mind that it's Postgres SQL.
17 |
18 | ### Quick example
19 |
20 |
21 | type postgres
22 | host master.db.service.local
23 | # port 3306 # default
24 | database application_logs
25 | username myuser
26 | password mypass
27 | key_names status,bytes,vhost,path,rhost,agent,referer
28 | sql INSERT INTO accesslog (status,bytes,vhost,path,rhost,agent,referer) VALUES ($1,$2,$3,$4,$5,$6,$7)
29 | flush_intervals 5s
30 |
31 |
32 |
33 |
34 | ## Component
35 |
36 | ### PostgresOutput
37 |
38 | Plugin to store Postgres tables over SQL, to each columns per values, or to single column as json.
39 |
40 | ## Configuration
41 |
42 | ### MysqlOutput
43 |
44 | MysqlOutput needs MySQL server's host/port/database/username/password, and INSERT format as SQL, or as table name and columns.
45 |
46 |
47 | type mysql
48 | host master.db.service.local
49 | # port 3306 # default
50 | database application_logs
51 | username myuser
52 | password mypass
53 | key_names status,bytes,vhost,path,rhost,agent,referer
54 | sql INSERT INTO accesslog (status,bytes,vhost,path,rhost,agent,referer) VALUES (?,?,?,?,?,?,?)
55 | flush_intervals 5s
56 |
57 |
58 |
59 | type mysql
60 | host master.db.service.local
61 | database application_logs
62 | username myuser
63 | password mypass
64 | key_names status,bytes,vhost,path,rhost,agent,referer
65 | table accesslog
66 | # 'columns' names order must be same with 'key_names'
67 | columns status,bytes,vhost,path,rhost,agent,referer
68 | flush_intervals 5s
69 |
70 |
71 | Or, insert json into single column.
72 |
73 |
74 | type mysql
75 | host master.db.service.local
76 | database application_logs
77 | username root
78 | table accesslog
79 | columns jsondata
80 | format json
81 | flush_intervals 5s
82 |
83 |
84 | To include time/tag into output, use `include_time_key` and `include_tag_key`, like this:
85 |
86 |
87 | type mysql
88 | host my.mysql.local
89 | database anydatabase
90 | username yourusername
91 | password secret
92 |
93 | include_time_key yes
94 | ### default `time_format` is ISO-8601
95 | # time_format %Y%m%d-%H%M%S
96 | ### default `time_key` is 'time'
97 | # time_key timekey
98 |
99 | include_tag_key yes
100 | ### default `tag_key` is 'tag'
101 | # tag_key tagkey
102 |
103 | table anydata
104 | key_names time,tag,field1,field2,field3,field4
105 | sql INSERT INTO baz (coltime,coltag,col1,col2,col3,col4) VALUES (?,?,?,?,?,?)
106 |
107 |
108 | Or, for json:
109 |
110 |
111 | type mysql
112 | host database.local
113 | database foo
114 | username root
115 |
116 | include_time_key yes
117 | utc # with UTC timezome output (default: localtime)
118 | time_format %Y%m%d-%H%M%S
119 | time_key timeattr
120 |
121 | include_tag_key yes
122 | tag_key tagattr
123 | table accesslog
124 | columns jsondata
125 | format json
126 |
127 | #=> inserted json data into column 'jsondata' with addtional attribute 'timeattr' and 'tagattr'
128 |
129 | ## TODO
130 |
131 | * implement 'tag_mapped'
132 | * dynamic tag based table selection
133 |
134 | ## Copyright
135 |
136 | * Copyright
137 | * Copyright 2013 Uken Games
138 | * License
139 | * Apache License, Version 2.0
140 |
--------------------------------------------------------------------------------
/Rakefile:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env rake
2 | require "bundler/gem_tasks"
3 |
4 | require 'rake/testtask'
5 | Rake::TestTask.new(:test) do |test|
6 | test.libs << 'lib' << 'test'
7 | test.pattern = 'test/**/test_*.rb'
8 | test.verbose = true
9 | end
10 |
11 | task :default => :test
12 |
--------------------------------------------------------------------------------
/fluent-plugin-postgres.gemspec:
--------------------------------------------------------------------------------
1 | # -*- encoding: utf-8 -*-
2 | Gem::Specification.new do |s|
3 | s.name = 'fluent-plugin-postgres'
4 | s.version = '0.1.0'
5 | s.authors = ['TAGOMORI Satoshi', 'Diogo Terror', 'pitr']
6 | s.email = ['team@uken.com']
7 | s.description = %q{fluent plugin to insert on PostgreSQL}
8 | s.summary = %q{fluent plugin to insert on PostgreSQL}
9 | s.homepage = 'https://github.com/uken/fluent-plugin-postgres'
10 | s.license = 'Apache-2.0'
11 |
12 | s.files = `git ls-files`.split($\)
13 | s.executables = s.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
14 | s.test_files = s.files.grep(%r{^(test|spec|features)/})
15 | s.require_paths = ['lib']
16 |
17 | s.add_dependency 'fluentd', ['>= 0.14.15', '< 2']
18 | s.add_dependency 'pg'
19 |
20 | s.add_development_dependency 'rake'
21 | s.add_development_dependency 'test-unit', '~> 3.2.0'
22 | end
23 |
--------------------------------------------------------------------------------
/lib/fluent/plugin/out_postgres.rb:
--------------------------------------------------------------------------------
1 | require 'fluent/plugin/output'
2 | require 'pg'
3 |
4 | class Fluent::Plugin::PostgresOutput < Fluent::Plugin::Output
5 | Fluent::Plugin.register_output('postgres', self)
6 |
7 | helpers :inject, :compat_parameters
8 |
9 | config_param :host, :string
10 | config_param :port, :integer, :default => nil
11 | config_param :database, :string
12 | config_param :username, :string
13 | config_param :password, :string, :default => ''
14 |
15 | config_param :key_names, :string, :default => nil # nil allowed for json format
16 | config_param :sql, :string, :default => nil
17 | config_param :table, :string, :default => nil
18 | config_param :columns, :string, :default => nil
19 |
20 | config_param :format, :string, :default => "raw" # or json
21 |
22 | attr_accessor :handler
23 |
24 | # We don't currently support mysql's analogous json format
25 | def configure(conf)
26 | compat_parameters_convert(conf, :inject)
27 | super
28 |
29 | if @format == 'json'
30 | @format_proc = Proc.new{|tag, time, record| record.to_json}
31 | else
32 | @key_names = @key_names.split(/\s*,\s*/)
33 | @format_proc = Proc.new{|tag, time, record| @key_names.map{|k| record[k]}}
34 | end
35 |
36 | if @columns.nil? and @sql.nil?
37 | raise Fluent::ConfigError, "columns or sql MUST be specified, but missing"
38 | end
39 | if @columns and @sql
40 | raise Fluent::ConfigError, "both of columns and sql are specified, but specify one of them"
41 | end
42 | end
43 |
44 | def start
45 | super
46 | end
47 |
48 | def shutdown
49 | super
50 | end
51 |
52 | def format(tag, time, record)
53 | record = inject_values_to_record(tag, time, record)
54 | [tag, time, @format_proc.call(tag, time, record)].to_msgpack
55 | end
56 |
57 | def multi_workers_ready?
58 | true
59 | end
60 |
61 | def formatted_to_msgpack_binary?
62 | true
63 | end
64 |
65 | def client
66 | PG::Connection.new({
67 | :host => @host, :port => @port,
68 | :user => @username, :password => @password,
69 | :dbname => @database
70 | })
71 | end
72 |
73 | def write(chunk)
74 | handler = self.client
75 | handler.prepare("write", @sql)
76 | chunk.msgpack_each { |tag, time, data|
77 | handler.exec_prepared("write", data)
78 | }
79 | handler.close
80 | end
81 | end
82 |
--------------------------------------------------------------------------------
/test/helper.rb:
--------------------------------------------------------------------------------
1 | require 'rubygems'
2 | require 'bundler'
3 | begin
4 | Bundler.setup(:default, :development)
5 | rescue Bundler::BundlerError => e
6 | $stderr.puts e.message
7 | $stderr.puts "Run `bundle install` to install missing gems"
8 | exit e.status_code
9 | end
10 | require 'test/unit'
11 |
12 | $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
13 | $LOAD_PATH.unshift(File.dirname(__FILE__))
14 | require 'fluent/test'
15 | require 'fluent/test/driver/output'
16 | require 'fluent/test/helpers'
17 | unless ENV.has_key?('VERBOSE')
18 | nulllogger = Object.new
19 | nulllogger.instance_eval {|obj|
20 | def method_missing(method, *args)
21 | # pass
22 | end
23 | }
24 | $log = nulllogger
25 | end
26 |
27 | require 'fluent/plugin/out_postgres'
28 |
29 | class Test::Unit::TestCase
30 | include Fluent::Test::Helpers
31 | end
32 |
--------------------------------------------------------------------------------
/test/plugin/test_out_postgres.rb:
--------------------------------------------------------------------------------
1 | require 'helper'
2 | require 'pg'
3 |
4 | class PostgresOutputTest < Test::Unit::TestCase
5 | def setup
6 | Fluent::Test.setup
7 | end
8 |
9 | CONFIG = %[
10 | host database.local
11 | database foo
12 | username bar
13 | password mogera
14 | key_names field1,field2,field3
15 | sql INSERT INTO baz (col1,col2,col3,col4) VALUES (?,?,?,?)
16 | ]
17 |
18 | def create_driver(conf=CONFIG)
19 | d = Fluent::Test::Driver::Output.new(Fluent::Plugin::PostgresOutput).configure(conf)
20 | d.instance.instance_eval {
21 | def client
22 | obj = Object.new
23 | obj.instance_eval {
24 | def prepare(*args); true; end
25 | def exec_prepared(*args); true; end
26 | def close; true; end
27 | }
28 | obj
29 | end
30 | }
31 | d
32 | end
33 |
34 | def test_configure_fails_if_both_cols_and_sql_specified
35 | assert_raise(Fluent::ConfigError) {
36 | create_driver %[
37 | host database.local
38 | database foo
39 | username bar
40 | password mogera
41 | key_names field1,field2,field3
42 | sql INSERT INTO baz (col1,col2,col3,col4) VALUES (?,?,?,?)
43 | columns col1,col2,col3,col4
44 | ]
45 | }
46 | end
47 |
48 | def test_configure_fails_if_neither_cols_or_sql_specified
49 | assert_raise(Fluent::ConfigError) {
50 | create_driver %[
51 | host database.local
52 | database foo
53 | username bar
54 | password mogera
55 | key_names field1,field2,field3
56 | ]
57 | }
58 | end
59 |
60 | def test_key_names_with_spaces
61 | d = create_driver %[
62 | host database.local
63 | database foo
64 | username bar
65 | password mogera
66 | table baz
67 | key_names time, tag, field1, field2, field3, field4
68 | sql INSERT INTO baz (coltime,coltag,col1,col2,col3,col4) VALUES (?,?,?,?,?,?)
69 | ]
70 | assert_equal ["time", "tag", "field1", "field2", "field3", "field4"], d.instance.key_names
71 | end
72 |
73 | def test_time_and_tag_key
74 | d = create_driver %[
75 | host database.local
76 | database foo
77 | username bar
78 | password mogera
79 | include_time_key yes
80 | utc
81 | include_tag_key yes
82 | table baz
83 | key_names time,tag,field1,field2,field3,field4
84 | sql INSERT INTO baz (coltime,coltag,col1,col2,col3,col4) VALUES (?,?,?,?,?,?)
85 | ]
86 | assert_equal 'INSERT INTO baz (coltime,coltag,col1,col2,col3,col4) VALUES (?,?,?,?,?,?)', d.instance.sql
87 |
88 | time = event_time('2012-12-17 01:23:45 UTC')
89 | record = {'field1'=>'value1','field2'=>'value2','field3'=>'value3','field4'=>'value4'}
90 | d.run(default_tag: 'test') do
91 | d.feed(time, record)
92 | end
93 | assert_equal ['test', time, ['2012-12-17T01:23:45Z','test','value1','value2','value3','value4']].to_msgpack, d.formatted[0]
94 | end
95 |
96 | def test_time_and_tag_key_complex
97 | d = create_driver %[
98 | host database.local
99 | database foo
100 | username bar
101 | password mogera
102 | include_time_key yes
103 | utc
104 | time_format %Y%m%d-%H%M%S
105 | time_key timekey
106 | include_tag_key yes
107 | tag_key tagkey
108 | table baz
109 | key_names timekey,tagkey,field1,field2,field3,field4
110 | sql INSERT INTO baz (coltime,coltag,col1,col2,col3,col4) VALUES (?,?,?,?,?,?)
111 | ]
112 | assert_equal 'INSERT INTO baz (coltime,coltag,col1,col2,col3,col4) VALUES (?,?,?,?,?,?)', d.instance.sql
113 |
114 | time = event_time('2012-12-17 09:23:45 +0900')
115 | record = {'field1'=>'value1','field2'=>'value2','field3'=>'value3','field4'=>'value4'}
116 | d.run(default_tag: 'test') do
117 | d.feed(time, record)
118 | end
119 | assert_equal ['test', time, ['20121217-002345','test','value1','value2','value3','value4']].to_msgpack, d.formatted[0]
120 | end
121 | end
122 |
--------------------------------------------------------------------------------