fluent-plugin-postgresql-csvlog 0.1.0 → 0.3.2

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3f63769c3a4c78fc3db074b812e6f75e77c46b355563b403c3de620c49935392
4
- data.tar.gz: 2fd8d1acd1fac5ce24b72ed4d488fc5f80f0a8752084784cf16f419389f34499
3
+ metadata.gz: 5d61fc31718e43c6d1dff46139a1b03d56384405d45b69c9772dc6f7b6a66dbf
4
+ data.tar.gz: 5d2d23a4a7b5f277b19f181aa515ab74718f6ef93824e2c958264d9bbdf9c9aa
5
5
  SHA512:
6
- metadata.gz: 34fefa81b79223bf1840481f6ffd232e4d343a1eed9876f8f58893f686c8c4a1c1fbd37679c1a8960bcddef3a992733fe4b53158ee7558c10b49ac42a1793996
7
- data.tar.gz: d105daf33a577b40e0035bc2a6d512390c361bb42bb9a11c2290e7489f04f813338779b45682969fa7616e8d7bb319db04dfec1eaf624586e50709c4a71eba83
6
+ metadata.gz: f2106f60749b6fa8fc931ccd3d85f51595b0ec60eb9f425935072b1749ed068e8a068c6d513f1557a20e8c5a0613acad80c6690c7ce2aa20cca3027d686c388a
7
+ data.tar.gz: 9ceef623cbd5256e047dea817d487d2711cc8b387ff068a10913100fe649e77174a600ac019b4729403390152c1a3e6f162accf4cc50f020561032b6493d965f
data/.gitlab-ci.yml CHANGED
@@ -9,3 +9,21 @@ test:
9
9
  cache:
10
10
  paths:
11
11
  - vendor/ruby
12
+
13
+ # integration tests
14
+ itest:
15
+ services:
16
+ - name: postgres:12
17
+ alias: postgres
18
+ command: ["postgres", "-c", "shared_preload_libraries=pg_stat_statements", "-c", "pg_stat_statements.track=all"]
19
+ variables:
20
+ POSTGRES_USER: testuser
21
+ POSTGRES_PASSWORD: testpass
22
+ before_script:
23
+ - bundle config set path vendor
24
+ - bundle install --jobs $(nproc)
25
+ script:
26
+ - bundle exec rake itest
27
+ cache:
28
+ paths:
29
+ - vendor/ruby
data/Dockerfile ADDED
@@ -0,0 +1,18 @@
1
+ # Dockerfile useful for manual testing purposes
2
+ FROM fluent/fluentd:v1.13-1
3
+ USER root
4
+ RUN apk add bash alpine-sdk postgresql-dev postgresql-client ruby-dev
5
+
6
+ WORKDIR /src/
7
+
8
+ ADD Gemfile /src/
9
+ ADD fluent-plugin-postgresql-csvlog.gemspec /src/
10
+
11
+ ADD . /src/
12
+
13
+ RUN bundle install
14
+ RUN rake build
15
+
16
+ RUN fluent-gem install pkg/*.gem
17
+
18
+ ENTRYPOINT [ "/bin/bash"]
data/README.md CHANGED
@@ -47,11 +47,13 @@ ingest and parse PostgreSQL CSV logs:
47
47
 
48
48
  <filter postgres.postgres_csv>
49
49
  @type postgresql_slowlog
50
+ output_key query
50
51
  </filter>
51
52
 
52
53
  <filter postgres.postgres_csv>
53
54
  @type postgresql_redactor
54
- key sql
55
+ input_key query
56
+ output_key sql
55
57
  fingerprint_key fingerprint
56
58
  </filter>
57
59
 
data/Rakefile CHANGED
@@ -9,4 +9,10 @@ Rake::TestTask.new(:test) do |test|
9
9
  test.verbose = true
10
10
  end
11
11
 
12
+ Rake::TestTask.new(:itest) do |test|
13
+ test.libs << 'lib' << 'test'
14
+ test.test_files = FileList['test/**/itest_*.rb']
15
+ test.verbose = true
16
+ end
17
+
12
18
  task :default => [:build]
@@ -0,0 +1,19 @@
1
+ # Docker Compose setup useful for testing and development purposes
2
+ version: "3.9"
3
+ services:
4
+ fluentd:
5
+ build: .
6
+ links:
7
+ - postgres
8
+ entrypoint: /usr/bin/fluentd -vvv -c /src/example-fluentd.conf
9
+ postgres:
10
+ image: postgres
11
+ restart: always
12
+ environment:
13
+ - POSTGRES_USER=testuser
14
+ - POSTGRES_PASSWORD=testpass
15
+ ports:
16
+ - '5438:5432'
17
+ command: postgres -c shared_preload_libraries=pg_stat_statements -c pg_stat_statements.track=all
18
+ volumes:
19
+ - ./sql/create_extension.sql:/docker-entrypoint-initdb.d/create_extension.sql
@@ -0,0 +1,12 @@
1
+ <source>
2
+ @type pg_stat_statements
3
+ tag postgres.pg_stat_statements
4
+ host postgres
5
+ username testuser
6
+ password testpass
7
+ interval 1
8
+ </source>
9
+
10
+ <match postgres.pg_stat_statements>
11
+ @type stdout
12
+ </match>
@@ -2,10 +2,10 @@ $:.push File.expand_path('lib', __dir__)
2
2
 
3
3
  Gem::Specification.new do |s|
4
4
  s.name = 'fluent-plugin-postgresql-csvlog'
5
- s.version = '0.1.0'
5
+ s.version = '0.3.2'
6
6
  s.authors = ['stanhu']
7
7
  s.email = ['stanhu@gmail.com']
8
- s.homepage = 'https://gitlab.com/gitlab-org/fluent-plugin-postgresql-csvlog'
8
+ s.homepage = 'https://gitlab.com/gitlab-org/fluent-plugins/fluent-plugin-postgresql-csvlog'
9
9
  s.summary = 'fluentd plugins to work with PostgreSQL CSV logs'
10
10
  s.description = 'fluentd plugins to work with PostgreSQL CSV logs'
11
11
 
@@ -15,6 +15,7 @@ Gem::Specification.new do |s|
15
15
  s.require_paths = ['lib']
16
16
 
17
17
  s.add_dependency 'fluentd', ['>= 1.0', '< 2']
18
+ s.add_dependency 'pg', '~> 1.1'
18
19
  s.add_dependency 'pg_query', '~> 2.0'
19
20
 
20
21
  s.add_development_dependency 'rake'
@@ -7,23 +7,25 @@ module Fluent::Plugin
7
7
  class PostgreSQLRedactor < Filter
8
8
  Fluent::Plugin.register_filter('postgresql_redactor', self)
9
9
 
10
- desc 'Field to parse for SQL queries'
11
- config_param :key, :string, default: 'sql'
10
+ desc 'Input field to parse for SQL queries'
11
+ config_param :input_key, :string, default: 'query'
12
+
13
+ desc 'Output field to store SQL queries'
14
+ config_param :output_key, :string, default: 'sql'
12
15
 
13
16
  desc 'Name of field to store SQL query fingerprint'
14
17
  config_param :fingerprint_key, :string, default: 'fingerprint'
15
18
 
16
19
  def filter(_tag, _time, record)
17
- statement = record['statement']
20
+ statement = record[@input_key]
18
21
 
19
22
  return record unless statement
20
23
 
21
24
  normalized = PgQuery.normalize(statement)
22
25
  record[@fingerprint_key] = PgQuery.parse(normalized).fingerprint if @fingerprint_key
23
26
 
24
- record.delete('statement')
25
- record[@key] = normalized
26
- record.delete('message')
27
+ record.delete(@input_key)
28
+ record[@output_key] = normalized
27
29
 
28
30
  record
29
31
  rescue PgQuery::ParseError
@@ -12,6 +12,9 @@ module Fluent
12
12
  class PostgreSQLSlowLog < Filter
13
13
  Fluent::Plugin.register_filter('postgresql_slowlog', self)
14
14
 
15
+ desc 'Field to output SQL queries'
16
+ config_param :output_key, :string, default: 'query'
17
+
15
18
  SLOWLOG_REGEXP = /^duration: (\d+(?:\.\d+)?) ms .*?:\s*(.*)/m.freeze
16
19
 
17
20
  def filter(_tag, _time, record)
@@ -20,7 +23,8 @@ module Fluent
20
23
  # rubocop:disable Style/PerlBackrefs
21
24
  if record['message'] =~ SLOWLOG_REGEXP
22
25
  record['duration_s'] = $1.to_f / 1000.0
23
- record['statement'] = $2
26
+ record[@output_key] = $2
27
+ record.delete('message')
24
28
  end
25
29
  # rubocop:enable Style/PerlBackrefs
26
30
 
@@ -0,0 +1,136 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'fluent/plugin/input'
4
+ require 'pg'
5
+ require 'pg_query'
6
+
7
+ module Fluent::Plugin
8
+ # PgStatStatementsInput will periodically poll postgres, querying pg_stat_statements
9
+ # for queryid to query mappings. These are then normalized for security purposes
10
+ # fingerprinted and emitted as records with the following format:
11
+ # {
12
+ # 'fingerprint' => '8a6e9896bd9048a2',
13
+ # 'query' => 'SELECT * FROM table ORDER BY queryid LIMIT $1',
14
+ # 'query_length' => 58,
15
+ # 'queryid' => 3239318621761098074
16
+ # }
17
+ class PgStatStatementsInput < Input
18
+ Fluent::Plugin.register_input('pg_stat_statements', self)
19
+
20
+ desc 'PostgreSQL host'
21
+ config_param :host, :string
22
+
23
+ desc 'RDBMS port (default: 5432)'
24
+ config_param :port, :integer, default: 5432
25
+
26
+ desc 'login user name'
27
+ config_param :username, :string, default: nil
28
+
29
+ desc 'postgres db'
30
+ config_param :dbname, :string, default: nil
31
+
32
+ desc 'login password'
33
+ config_param :password, :string, default: nil, secret: true
34
+
35
+ # See https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNECT-SSLMODE
36
+ # for options
37
+ desc 'postgres sslmode'
38
+ config_param :sslmode, :string, default: 'prefer'
39
+
40
+ desc 'tag'
41
+ config_param :tag, :string, default: nil
42
+
43
+ desc 'interval in second to run query'
44
+ config_param :interval, :time, default: 300
45
+
46
+ desc 'Name of field to store SQL query fingerprint'
47
+ config_param :fingerprint_key, :string, default: 'fingerprint'
48
+
49
+ def start
50
+ @stop_flag = false
51
+ @thread = Thread.new(&method(:thread_main))
52
+ end
53
+
54
+ def shutdown
55
+ @stop_flag = true
56
+
57
+ # Interrupt thread and wait for it to finish
58
+ Thread.new { @thread.run } if @thread
59
+ @thread.join
60
+ end
61
+
62
+ def thread_main
63
+ until @stop_flag
64
+ sleep @interval
65
+ break if @stop_flag
66
+
67
+ begin
68
+ with_connection do |conn|
69
+ emit_statements_to_stream(conn)
70
+ end
71
+ rescue StandardError => e
72
+ log.error 'unexpected error', error: e.message, error_class: e.class
73
+ log.error_backtrace e.backtrace
74
+ end
75
+ end
76
+ end
77
+
78
+ # Returns a fluentd record for a query row
79
+ def record_for_row(row)
80
+ query = row['query']
81
+
82
+ # We record the query_length as it will help in understanding whether unparseable
83
+ # queries are truncated.
84
+ record = { 'queryid' => row['queryid'], 'query_length' => query&.length }
85
+
86
+ return record unless query
87
+
88
+ normalized = PgQuery.normalize(query)
89
+ record['query'] = normalized
90
+
91
+ record[@fingerprint_key] = PgQuery.parse(normalized).fingerprint if @fingerprint_key
92
+
93
+ record
94
+ rescue PgQuery::ParseError
95
+ record['query_unparseable'] = true
96
+
97
+ record
98
+ end
99
+
100
+ private
101
+
102
+ # Query the database and emit statements to fluentd router
103
+ def emit_statements_to_stream(conn)
104
+ me = Fluent::MultiEventStream.new
105
+
106
+ now = Fluent::Engine.now
107
+ conn.exec('SELECT queryid, query FROM public.pg_stat_statements').each do |row|
108
+ record = record_for_row(row)
109
+ me.add(now, record)
110
+ end
111
+
112
+ @router.emit_stream(@tag, me)
113
+ end
114
+
115
+ # Since this query is very infrequent, and it may be communicating directly
116
+ # with postgres without pgbouncer, don't use a persistent connection and
117
+ # ensure that it is properly closed
118
+ def with_connection(&block)
119
+ conn = PG.connect(
120
+ host: @host,
121
+ dbname: @dbname,
122
+ sslmode: @sslmode,
123
+ user: @username,
124
+ password: @password
125
+ )
126
+ conn.type_map_for_results = PG::BasicTypeMapForResults.new conn
127
+
128
+ begin
129
+ block.call(conn)
130
+ ensure
131
+ # Always close the connection
132
+ conn.finish
133
+ end
134
+ end
135
+ end
136
+ end
@@ -0,0 +1 @@
1
+ CREATE EXTENSION pg_stat_statements;
data/test/helper.rb CHANGED
@@ -7,10 +7,13 @@ $LOAD_PATH.unshift(File.join(__dir__, '..', 'lib'))
7
7
  $LOAD_PATH.unshift(__dir__)
8
8
  require 'fluent/test'
9
9
  require 'fluent/test/driver/filter'
10
+ require 'fluent/test/driver/input'
10
11
  require 'fluent/test/helpers'
11
12
 
12
13
  Test::Unit::TestCase.include(Fluent::Test::Helpers)
14
+ Test::Unit::TestCase.extend(Fluent::Test::Helpers)
13
15
 
14
16
  require 'fluent/plugin/filter_postgresql_slowlog'
15
17
  require 'fluent/plugin/filter_postgresql_redactor'
16
18
  require 'fluent/plugin/filter_marginalia'
19
+ require 'fluent/plugin/in_pg_stat_statements'
@@ -0,0 +1,120 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../helper'
4
+
5
+ class PgStatStatementsInputIntegrationTest < Test::Unit::TestCase
6
+ # The defaults values work with the configuration in .gitlab-ci.yml on the postgres service
7
+ # Override with env vars for local development
8
+ HOST = ENV.fetch('PG_TEST_HOST', 'postgres')
9
+ USERNAME = ENV.fetch('PG_TEST_USER', 'testuser')
10
+ PASSWORD = ENV.fetch('PG_TEST_PASSWORD', 'testpass')
11
+
12
+ def setup
13
+ Fluent::Test.setup
14
+
15
+ @conn = PG.connect(
16
+ host: HOST,
17
+ user: USERNAME,
18
+ password: PASSWORD
19
+ )
20
+
21
+ try_setup_extension
22
+ create_known_statement
23
+ end
24
+
25
+ def teardown
26
+ @conn&.finish
27
+ end
28
+
29
+ # Setup pg_stat_statements extension
30
+ def try_setup_extension
31
+ @conn.exec('CREATE EXTENSION pg_stat_statements')
32
+ rescue PG::DuplicateObject
33
+ end
34
+
35
+ # This statement gives us something to look for in the emitted stream
36
+ def create_known_statement
37
+ @conn.exec('SELECT * FROM pg_stat_statements ORDER BY queryid LIMIT 1')
38
+ end
39
+
40
+ VALID_CONFIG = %(
41
+ tag postgres.pg_stat_statements
42
+ host #{HOST}
43
+ username #{USERNAME}
44
+ password #{PASSWORD}
45
+ interval 1
46
+ )
47
+
48
+ INVALID_CONFIG = %(
49
+ host 'invalid_host.dne'
50
+ port 1234
51
+ username #{USERNAME}
52
+ password #{PASSWORD}
53
+ interval 1
54
+ )
55
+
56
+ def create_driver(config)
57
+ Fluent::Test::InputTestDriver.new(Fluent::Plugin::PgStatStatementsInput).configure(config)
58
+ end
59
+
60
+ sub_test_case 'configuration' do
61
+ test 'connects' do
62
+ d = create_driver(VALID_CONFIG)
63
+
64
+ emits = []
65
+ # wait 50 * 0.05, "see fluentd/lib/fluent/test/base.rb:79 num_waits.times { sleep 0.05 }
66
+ d.run(num_waits = 50) do
67
+ emits = d.emits
68
+ end
69
+
70
+ assert_false emits.empty?
71
+ end
72
+
73
+ # Why do we have this test? If postgres is still starting up, we don't want to cause the
74
+ # the fluentd configuration to fail. We would rather retry until we get a connection
75
+ test 'connects for an invalid config' do
76
+ d = create_driver(INVALID_CONFIG)
77
+
78
+ emits = []
79
+ # wait 50 * 0.05, "see fluentd/lib/fluent/test/base.rb:79 num_waits.times { sleep 0.05 }
80
+ d.run(num_waits = 50) do
81
+ emits = d.emits
82
+ end
83
+
84
+ assert_true emits.empty?
85
+ end
86
+ end
87
+
88
+ sub_test_case 'execution' do
89
+ test 'connects' do
90
+ d = create_driver(VALID_CONFIG)
91
+
92
+ emits = []
93
+ # wait 50 * 0.05, "see fluentd/lib/fluent/test/base.rb:79 num_waits.times { sleep 0.05 }
94
+ d.run(num_waits = 50) do
95
+ emits = d.emits
96
+ end
97
+
98
+ expected_record = {
99
+ 'fingerprint' => '8a6e9896bd9048a2',
100
+ 'query' => 'SELECT * FROM pg_stat_statements ORDER BY queryid LIMIT $1',
101
+ 'query_length' => 58,
102
+ 'queryid' => 3_239_318_621_761_098_074
103
+ }
104
+ known_statement_event = emits.find do |event|
105
+ record = event[2]
106
+ record['query'] == expected_record['query']
107
+ end
108
+
109
+ assert_false known_statement_event.nil?
110
+
111
+ tag = known_statement_event[0]
112
+ record = known_statement_event[2]
113
+
114
+ assert_equal 'postgres.pg_stat_statements', tag
115
+ assert_equal expected_record['fingerprint'], record['fingerprint']
116
+ assert_equal expected_record['query_length'], record['query_length']
117
+ assert_true expected_record.include? 'queryid'
118
+ end
119
+ end
120
+ end
@@ -22,7 +22,8 @@ class Marginalia < Test::Unit::TestCase
22
22
  inputs = [
23
23
  { 'statement' => 'SELECT * FROM projects' },
24
24
  { 'statement' => 'SELECT COUNT(*) FROM "projects" /* this is just a comment */' },
25
- { 'statement' => 'SELECT COUNT(*) FROM "projects" /*application:sidekiq,correlation_id:d67cae54c169e0cab7d73389e2934f0e,jid:52a1c8a9e4c555ea573f20f0,job_class:Geo::MetricsUpdateWorker*/' }
25
+ { 'statement' => 'SELECT COUNT(*) FROM "projects" /*application:sidekiq,correlation_id:d67cae54c169e0cab7d73389e2934f0e,jid:52a1c8a9e4c555ea573f20f0,job_class:Geo::MetricsUpdateWorker*/' },
26
+ { 'statement' => 'SELECT COUNT(*) FROM "projects" /*application:web,correlation_id:01F1D2T1SC9DM82A4865ATG1CP,endpoint_id:POST /api/:version/groups/:id/-/packages/mavenpath/:file_name*/' }
26
27
  ]
27
28
 
28
29
  d.run(default_tag: @tag) do
@@ -40,10 +41,19 @@ class Marginalia < Test::Unit::TestCase
40
41
  }
41
42
  ),
42
43
  d.filtered[2].last)
44
+ assert_equal(inputs[3].merge(
45
+ {
46
+ 'application' => 'web',
47
+ 'correlation_id' => '01F1D2T1SC9DM82A4865ATG1CP',
48
+ 'endpoint_id' => 'POST /api/:version/groups/:id/-/packages/mavenpath/:file_name'
49
+ }
50
+ ),
51
+ d.filtered[3].last)
43
52
 
44
53
  assert_equal('SELECT * FROM projects', d.filtered[0].last['statement'])
45
54
  assert_equal('SELECT COUNT(*) FROM "projects"', d.filtered[1].last['statement'])
46
55
  assert_equal('SELECT COUNT(*) FROM "projects"', d.filtered[2].last['statement'])
56
+ assert_equal('SELECT COUNT(*) FROM "projects"', d.filtered[3].last['statement'])
47
57
  end
48
58
 
49
59
  test 'parses prepended Marginalia comments' do
@@ -52,6 +62,7 @@ class Marginalia < Test::Unit::TestCase
52
62
  inputs = [
53
63
  { 'statement' => '/* this is just a comment */ SELECT COUNT(*) FROM "projects"' },
54
64
  { 'statement' => '/*application:sidekiq,correlation_id:d67cae54c169e0cab7d73389e2934f0e,jid:52a1c8a9e4c555ea573f20f0,job_class:Geo::MetricsUpdateWorker*/ SELECT COUNT(*) FROM "projects"' },
65
+ { 'statement' => '/*application:web,correlation_id:01F1D2T1SC9DM82A4865ATG1CP,endpoint_id:POST /api/:version/groups/:id/-/packages/mavenpath/:file_name*/ SELECT COUNT(*) FROM "projects"' },
55
66
  { 'statement' => '/*application:sidekiq*/ SELECT COUNT(*) FROM "projects"',
56
67
  'application' => 'test-conflict' }
57
68
  ]
@@ -72,10 +83,18 @@ class Marginalia < Test::Unit::TestCase
72
83
  d.filtered[1].last)
73
84
  assert_equal(inputs[2].merge(
74
85
  {
75
- 'statement_application' => 'sidekiq'
86
+ 'application' => 'web',
87
+ 'correlation_id' => '01F1D2T1SC9DM82A4865ATG1CP',
88
+ 'endpoint_id' => 'POST /api/:version/groups/:id/-/packages/mavenpath/:file_name'
76
89
  }
77
90
  ),
78
91
  d.filtered[2].last)
92
+ assert_equal(inputs[3].merge(
93
+ {
94
+ 'statement_application' => 'sidekiq'
95
+ }
96
+ ),
97
+ d.filtered[3].last)
79
98
 
80
99
  assert_equal('SELECT COUNT(*) FROM "projects"', d.filtered[0].last['statement'])
81
100
  assert_equal('SELECT COUNT(*) FROM "projects"', d.filtered[1].last['statement'])
@@ -8,11 +8,7 @@ class PostgreSQLRedactorTest < Test::Unit::TestCase
8
8
  @tag = 'test.tag'
9
9
  end
10
10
 
11
- CONFIG = '
12
- <filter test.tag>
13
- @type postgresql_redactor
14
- </filter>
15
- '
11
+ CONFIG = ''
16
12
 
17
13
  def create_driver(conf = CONFIG)
18
14
  Fluent::Test::Driver::Filter.new(Fluent::Plugin::PostgreSQLRedactor).configure(conf)
@@ -23,7 +19,7 @@ class PostgreSQLRedactorTest < Test::Unit::TestCase
23
19
 
24
20
  inputs = [
25
21
  { 'message' => 'duration: 2357.1 ms execute <unnamed>: SELECT * FROM projects WHERE id = 1',
26
- 'statement' => %(SELECT * FROM projects WHERE id = 1),
22
+ 'query' => %(SELECT * FROM projects WHERE id = 1),
27
23
  'duration_s' => 2.3571 }
28
24
  ]
29
25
 
@@ -31,20 +27,43 @@ class PostgreSQLRedactorTest < Test::Unit::TestCase
31
27
  inputs.each { |input| d.feed(input) }
32
28
  end
33
29
 
34
- assert_equal(%w[duration_s fingerprint sql], d.filtered[0].last.keys.sort)
30
+ assert_equal(%w[duration_s fingerprint message sql], d.filtered[0].last.keys.sort)
35
31
  assert_equal('SELECT * FROM projects WHERE id = $1', d.filtered[0].last['sql'])
36
32
  end
37
33
 
38
34
  test 'handles parse errors' do
39
35
  d = create_driver
40
36
 
41
- input = { 'statement' => 'create index something test (bla) include (bar)' }
37
+ input = { 'query' => 'create index something test (bla) include (bar)' }
42
38
 
43
39
  d.run(default_tag: @tag) do
44
40
  d.feed(input)
45
41
  end
46
42
 
47
- assert_equal(%w[pg_query_error statement], d.filtered[0].last.keys.sort)
48
- assert_equal(input['statement'], d.filtered[0].last['statement'])
43
+ assert_equal(%w[pg_query_error query], d.filtered[0].last.keys.sort)
44
+ assert_equal(input['query'], d.filtered[0].last['query'])
45
+ end
46
+
47
+ test 'uses configured input and output keys' do
48
+ d = create_driver(<<~CONF
49
+ input_key sql
50
+ output_key out_sql
51
+ CONF
52
+ )
53
+
54
+ inputs = [
55
+ {
56
+ 'message' => 'duration: 2357.1 ms execute <unnamed>: SELECT * FROM projects WHERE id = 1',
57
+ 'sql' => %(SELECT * FROM projects WHERE id = 1),
58
+ 'duration_s' => 2.3571
59
+ }
60
+ ]
61
+
62
+ d.run(default_tag: @tag) do
63
+ inputs.each { |input| d.feed(input) }
64
+ end
65
+
66
+ assert_equal(%w[duration_s fingerprint message out_sql], d.filtered[0].last.keys.sort)
67
+ assert_equal('SELECT * FROM projects WHERE id = $1', d.filtered[0].last['out_sql'])
49
68
  end
50
69
  end
@@ -32,18 +32,21 @@ class PostgreSQLSlowLogTest < Test::Unit::TestCase
32
32
 
33
33
  assert_equal(inputs[0].merge(
34
34
  {
35
- 'statement' => 'SELECT * FROM projects',
35
+ 'query' => 'SELECT * FROM projects',
36
36
  'duration_s' => 2.3571
37
37
  }
38
38
  ),
39
39
  d.filtered[0].last)
40
40
  assert_equal(inputs[1].merge(
41
41
  {
42
- 'statement' => 'SELECT COUNT(*) FROM "projects" /*application:sidekiq,correlation_id:d67cae54c169e0cab7d73389e2934f0e,jid:52a1c8a9e4c555ea573f20f0,job_class:Geo::MetricsUpdateWorker*/',
42
+ 'query' => 'SELECT COUNT(*) FROM "projects" /*application:sidekiq,correlation_id:d67cae54c169e0cab7d73389e2934f0e,jid:52a1c8a9e4c555ea573f20f0,job_class:Geo::MetricsUpdateWorker*/',
43
43
  'duration_s' => 1.873345
44
44
  }
45
45
  ),
46
46
  d.filtered[1].last)
47
+
48
+ assert_equal(%w[duration_s query], d.filtered[0].last.keys.sort)
49
+ assert_equal(%w[duration_s query], d.filtered[1].last.keys.sort)
47
50
  end
48
51
 
49
52
  test 'ignores messages not having to do with slow logs' do
@@ -55,5 +58,32 @@ class PostgreSQLSlowLogTest < Test::Unit::TestCase
55
58
  end
56
59
 
57
60
  assert_equal(input, d.filtered[0].last)
61
+ assert_equal(%w[message], d.filtered[0].last.keys.sort)
62
+ end
63
+
64
+ test 'outputs slow log entries to configured output key' do
65
+ d = create_driver(
66
+ <<~CONF
67
+ output_key my_key
68
+ CONF
69
+ )
70
+
71
+ inputs = [
72
+ { 'message' => 'duration: 2357.1 ms execute <unnamed>: SELECT * FROM projects' }
73
+ ]
74
+
75
+ d.run(default_tag: @tag) do
76
+ inputs.each { |input| d.feed(input) }
77
+ end
78
+
79
+ assert_equal(inputs[0].merge(
80
+ {
81
+ 'my_key' => 'SELECT * FROM projects',
82
+ 'duration_s' => 2.3571
83
+ }
84
+ ),
85
+ d.filtered[0].last)
86
+
87
+ assert_equal(%w[duration_s my_key], d.filtered[0].last.keys.sort)
58
88
  end
59
89
  end
@@ -0,0 +1,111 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../helper'
4
+
5
+ class PgStatStatementsInputTest < Test::Unit::TestCase
6
+ def setup
7
+ Fluent::Test.setup
8
+ end
9
+
10
+ CONFIG = %(
11
+ tag postgres.pg_stat_statements
12
+ host localhost
13
+ port 1234
14
+ dbname gitlab
15
+ sslmode require
16
+ username moo
17
+ password secret
18
+ interval 600
19
+ fingerprint_key fingerprint
20
+ )
21
+
22
+ def create_driver
23
+ Fluent::Test::InputTestDriver.new(Fluent::Plugin::PgStatStatementsInput).configure(CONFIG)
24
+ end
25
+
26
+ sub_test_case 'configuration' do
27
+ test 'basic configuration' do
28
+ d = create_driver
29
+
30
+ assert_equal 'postgres.pg_stat_statements', d.instance.tag
31
+ assert_equal 'localhost', d.instance.host
32
+ assert_equal 1234, d.instance.port
33
+ assert_equal 'gitlab', d.instance.dbname
34
+ assert_equal 'require', d.instance.sslmode
35
+ assert_equal 'moo', d.instance.username
36
+ assert_equal 'secret', d.instance.password
37
+ assert_equal 600, d.instance.interval
38
+ assert_equal 'fingerprint', d.instance.fingerprint_key
39
+ end
40
+ end
41
+
42
+ sub_test_case 'execution' do
43
+ test 'sql' do
44
+ d = create_driver
45
+ record = d.instance.record_for_row({ 'queryid' => '1234', 'query' => 'SELECT * FROM users WHERE user_id = ?' })
46
+
47
+ expected = {
48
+ 'fingerprint' => 'c071dee80d466e7d',
49
+ 'query' => 'SELECT * FROM users WHERE user_id = ?',
50
+ 'query_length' => 37,
51
+ 'queryid' => '1234'
52
+ }
53
+
54
+ assert_equal expected, record
55
+ end
56
+
57
+ test 'nil query' do
58
+ d = create_driver
59
+ record = d.instance.record_for_row({ 'queryid' => '1234', 'query' => nil })
60
+
61
+ expected = { 'query_length' => nil, 'queryid' => '1234' }
62
+ assert_equal expected, record
63
+ end
64
+
65
+ test 'ddl query' do
66
+ d = create_driver
67
+ ddl_sql = <<-SQL
68
+ CREATE TABLE accounts (
69
+ user_id serial PRIMARY KEY,
70
+ username VARCHAR(50) UNIQUE NOT NULL,
71
+ password VARCHAR(50) NOT NULL,
72
+ email VARCHAR(255) UNIQUE NOT NULL,
73
+ created_on TIMESTAMP NOT NULL,
74
+ last_login TIMESTAMP
75
+ )
76
+ SQL
77
+
78
+ record = d.instance.record_for_row({ 'queryid' => '1234', 'query' => ddl_sql })
79
+
80
+ expected = {
81
+ 'fingerprint' => 'fa9c9d26757c4f9b',
82
+ 'query' => ddl_sql,
83
+ 'query_length' => 287,
84
+ 'queryid' => '1234'
85
+ }
86
+ assert_equal expected, record
87
+ end
88
+
89
+ test 'set command' do
90
+ d = create_driver
91
+ record = d.instance.record_for_row({ 'queryid' => '1234', 'query' => "SET TIME ZONE 'PST8PDT'" })
92
+
93
+ expected = {
94
+ 'fingerprint' => '23f8d6eb1d3125c3',
95
+ 'query' => 'SET TIME ZONE $1',
96
+ 'query_length' => 23,
97
+ 'queryid' => '1234'
98
+ }
99
+
100
+ assert_equal expected, record
101
+ end
102
+
103
+ test 'unparseable sql' do
104
+ d = create_driver
105
+ record = d.instance.record_for_row({ 'queryid' => '1234', 'query' => 'SELECT * FROM' })
106
+
107
+ expected = { 'query_length' => 13, 'query_unparseable' => true, 'queryid' => '1234' }
108
+ assert_equal expected, record
109
+ end
110
+ end
111
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: fluent-plugin-postgresql-csvlog
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.3.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - stanhu
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2021-03-21 00:00:00.000000000 Z
11
+ date: 2021-07-12 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: fluentd
@@ -30,6 +30,20 @@ dependencies:
30
30
  - - "<"
31
31
  - !ruby/object:Gem::Version
32
32
  version: '2'
33
+ - !ruby/object:Gem::Dependency
34
+ name: pg
35
+ requirement: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: '1.1'
40
+ type: :runtime
41
+ prerelease: false
42
+ version_requirements: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - "~>"
45
+ - !ruby/object:Gem::Version
46
+ version: '1.1'
33
47
  - !ruby/object:Gem::Dependency
34
48
  name: pg_query
35
49
  requirement: !ruby/object:Gem::Requirement
@@ -80,21 +94,27 @@ extensions: []
80
94
  extra_rdoc_files: []
81
95
  files:
82
96
  - ".gitlab-ci.yml"
97
+ - Dockerfile
83
98
  - Gemfile
84
- - Gemfile.lock
85
99
  - LICENSE
86
100
  - README.md
87
101
  - Rakefile
102
+ - docker-compose.yml
103
+ - example-fluentd.conf
88
104
  - fluent-plugin-postgresql-csvlog.gemspec
89
105
  - lib/fluent/plugin/filter_marginalia.rb
90
106
  - lib/fluent/plugin/filter_postgresql_redactor.rb
91
107
  - lib/fluent/plugin/filter_postgresql_slowlog.rb
108
+ - lib/fluent/plugin/in_pg_stat_statements.rb
92
109
  - lib/fluent/plugin/parser_multiline_csv.rb
110
+ - sql/create_extension.sql
93
111
  - test/helper.rb
112
+ - test/plugin/itest_in_pg_stat_statements.rb
94
113
  - test/plugin/test_filter_marginalia.rb
95
114
  - test/plugin/test_filter_postgresql_redactor.rb
96
115
  - test/plugin/test_filter_postgresql_slowlog.rb
97
- homepage: https://gitlab.com/gitlab-org/fluent-plugin-postgresql-csvlog
116
+ - test/plugin/test_in_pg_stat_statements.rb
117
+ homepage: https://gitlab.com/gitlab-org/fluent-plugins/fluent-plugin-postgresql-csvlog
98
118
  licenses: []
99
119
  metadata: {}
100
120
  post_install_message:
@@ -118,6 +138,8 @@ specification_version: 4
118
138
  summary: fluentd plugins to work with PostgreSQL CSV logs
119
139
  test_files:
120
140
  - test/helper.rb
141
+ - test/plugin/itest_in_pg_stat_statements.rb
121
142
  - test/plugin/test_filter_marginalia.rb
122
143
  - test/plugin/test_filter_postgresql_redactor.rb
123
144
  - test/plugin/test_filter_postgresql_slowlog.rb
145
+ - test/plugin/test_in_pg_stat_statements.rb
data/Gemfile.lock DELETED
@@ -1,52 +0,0 @@
1
- PATH
2
- remote: .
3
- specs:
4
- fluent-plugin-postgresql-csvlog (0.1.0)
5
- fluentd (>= 1.0, < 2)
6
- pg_query (~> 2.0)
7
-
8
- GEM
9
- remote: https://rubygems.org/
10
- specs:
11
- concurrent-ruby (1.1.8)
12
- cool.io (1.7.1)
13
- fluentd (1.12.1)
14
- bundler
15
- cool.io (>= 1.4.5, < 2.0.0)
16
- http_parser.rb (>= 0.5.1, < 0.7.0)
17
- msgpack (>= 1.3.1, < 2.0.0)
18
- serverengine (>= 2.2.2, < 3.0.0)
19
- sigdump (~> 0.2.2)
20
- strptime (>= 0.2.2, < 1.0.0)
21
- tzinfo (>= 1.0, < 3.0)
22
- tzinfo-data (~> 1.0)
23
- yajl-ruby (~> 1.0)
24
- google-protobuf (3.15.6-universal-darwin)
25
- http_parser.rb (0.6.0)
26
- msgpack (1.4.2)
27
- pg_query (2.0.1)
28
- google-protobuf (~> 3.15.5)
29
- power_assert (2.0.0)
30
- rake (13.0.3)
31
- serverengine (2.2.3)
32
- sigdump (~> 0.2.2)
33
- sigdump (0.2.4)
34
- strptime (0.2.5)
35
- test-unit (3.4.0)
36
- power_assert
37
- tzinfo (2.0.4)
38
- concurrent-ruby (~> 1.0)
39
- tzinfo-data (1.2021.1)
40
- tzinfo (>= 1.0.0)
41
- yajl-ruby (1.4.1)
42
-
43
- PLATFORMS
44
- ruby
45
-
46
- DEPENDENCIES
47
- fluent-plugin-postgresql-csvlog!
48
- rake
49
- test-unit (~> 3.2)
50
-
51
- BUNDLED WITH
52
- 2.1.4