fluent-plugin-sentry-http 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 ADDED
@@ -0,0 +1,14 @@
1
+ .bundle
2
+ .yardoc
3
+ Gemfile.lock
4
+ _yardoc
5
+ coverage
6
+ doc
7
+ pkg
8
+ spec/reports
9
+ tmp
10
+ *.bundle
11
+ *.so
12
+ *.o
13
+ *.a
14
+ mkmf.log
data/.travis.yml ADDED
@@ -0,0 +1,6 @@
1
+ language: ruby
2
+ rvm:
3
+ - 1.9.3
4
+ - 2.0.0
5
+ - 2.1.0
6
+ - 2.2.0
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in fluent-plugin-sentry-http.gemspec
4
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,13 @@
1
+ Copyright (c) 2015- IKUTA Masahito
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.
data/README.md ADDED
@@ -0,0 +1,45 @@
1
+ # fluent-plugin-sentry-http
2
+
3
+ [fluentd](http://fluentd.org) input plugin that receive exceptions from [Sentry Clients](https://github.com/getsentry).
4
+
5
+ [![Build Status](https://travis-ci.org/cooldaemon/fluent-plugin-sentry-http.svg?branch=master)](https://travis-ci.org/cooldaemon/fluent-plugin-sentry-http)
6
+
7
+ ## Installation
8
+
9
+ Install with gem or fluent-gem command as:
10
+
11
+ ```
12
+ # for fluentd
13
+ $ gem install fluent-plugin-sentry-http
14
+
15
+ # for td-agent
16
+ $ sudo /usr/lib64/fluent/ruby/bin/fluent-gem install fluent-plugin-sentry-http
17
+ ```
18
+
19
+ ## Component
20
+
21
+ ### SentryHttpInput
22
+
23
+ Plugin to accept exception input from [Sentry Clients](https://github.com/getsentry).
24
+
25
+ #### Configuration
26
+
27
+ ```
28
+ <source>
29
+ type sentry_http
30
+ port 8888
31
+ bind 0.0.0.0
32
+ <application 999>
33
+ tag sentry.egg
34
+ user aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
35
+ pass bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb
36
+ </application>
37
+ </source>
38
+ ```
39
+
40
+ ## Copyright
41
+
42
+ - Copyright
43
+ - Copyright(C) 2015- IKUTA Masahito (cooldaemon)
44
+ - License
45
+ - Apache License, Version 2.0
data/Rakefile ADDED
@@ -0,0 +1,10 @@
1
+ require 'bundler/gem_tasks'
2
+
3
+ require 'rake/testtask'
4
+ Rake::TestTask.new(:test) do |test|
5
+ test.libs << 'lib' << 'test'
6
+ test.pattern = 'test/**/test_*.rb'
7
+ test.verbose = true
8
+ end
9
+
10
+ task :default => :test
@@ -0,0 +1,26 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'fluent/plugin/sentry_http/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "fluent-plugin-sentry-http"
8
+ spec.version = Fluent::Plugin::SentryHttp::VERSION
9
+ spec.authors = ["IKUTA Masahito"]
10
+ spec.email = ["cooldaemon@gmail.com"]
11
+ spec.summary = %q{Fluentd input plugin that receive exceptions from the Sentry clients(Raven).}
12
+ spec.description = spec.summary
13
+ spec.homepage = "http://github.com/cooldaemon/fluent-plugin-sentry-http"
14
+ spec.license = "APLv2"
15
+
16
+ spec.files = `git ls-files -z`.split("\x0")
17
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
+ spec.require_paths = ["lib"]
20
+
21
+ spec.add_development_dependency "rake", "~> 10.0"
22
+ spec.add_development_dependency "test-unit", ["~> 3.0", "~> 3.1"]
23
+
24
+ spec.add_dependency "fluentd", ">= 0.10.55"
25
+ spec.add_dependency "oj", ">= 1.4"
26
+ end
@@ -0,0 +1,103 @@
1
+ require 'fluent/plugin/in_http'
2
+
3
+ module Fluent
4
+ class SentryHttpInput < HttpInput
5
+ Plugin.register_input('sentry_http', self)
6
+
7
+ config_param :format, :string, :default => 'sentry_http'
8
+
9
+ attr_reader :mapping
10
+
11
+ def initialize
12
+ super
13
+ @mapping = {}
14
+ end
15
+
16
+ def configure(conf)
17
+ super
18
+ conf.elements.select {|element|
19
+ element.name == 'application'
20
+ }.each do |element|
21
+ @mapping[element.arg] = {
22
+ 'tag' => element['tag'],
23
+ 'user' => element['user'],
24
+ 'pass' => element['pass'],
25
+ }
26
+ end
27
+ end
28
+
29
+ def on_request(path_info, params)
30
+ begin
31
+ application = @mapping[path_info.split('/')[2]] # /api/999/store/
32
+ raise 'not found' unless application
33
+ rescue
34
+ return ['404 Not Found', {'Content-type' => 'text/plain'}, '']
35
+ end
36
+
37
+ begin
38
+ user, pass = get_auth_info(params)
39
+ raise 'unauthorized' unless application['user'] == user and application['pass'] == pass
40
+ rescue
41
+ return ['401 Unauthorized', {'Content-type' => 'text/plain'}, '']
42
+ end
43
+
44
+ begin
45
+ time, record = parse_params(params)
46
+ raise 'Record not found' if record.nil?
47
+
48
+ record['tag'] = application['tag']
49
+ record['time'] = time
50
+
51
+ if @add_http_headers
52
+ params.each_pair { |k, v|
53
+ if k.start_with?('HTTP_')
54
+ record[k] = v
55
+ end
56
+ }
57
+ end
58
+
59
+ if @add_remote_addr
60
+ record['REMOTE_ADDR'] = params['REMOTE_ADDR']
61
+ end
62
+ rescue
63
+ return ['400 Bad Request', {'Content-type' => 'text/plain'}, "400 Bad Request\n#{$!}\n"]
64
+ end
65
+
66
+ begin
67
+ router.emit(application['tag'], time, record)
68
+ rescue
69
+ return ['500 Internal Server Error', {'Content-type' => 'text/plain'}, "500 Internal Server Error\n#{$!}\n"]
70
+ end
71
+
72
+ return ['200 OK', {'Content-type' => 'text/plain'}, '']
73
+ end
74
+
75
+ private
76
+
77
+ def parse_params_with_parser(params)
78
+ if content = params[EVENT_RECORD_PARAMETER]
79
+ @parser.parse(content) { |time, record|
80
+ raise "Received event is not #{@format}: #{content}" if record.nil?
81
+ return time, record
82
+ }
83
+ else
84
+ raise "'#{EVENT_RECORD_PARAMETER}' parameter is required"
85
+ end
86
+ end
87
+
88
+ def get_auth_info(params)
89
+ user = nil
90
+ pass = nil
91
+ params['HTTP_X_SENTRY_AUTH'].split(', ').each do |element|
92
+ key, value = element.split('=')
93
+ case key
94
+ when 'sentry_key'
95
+ user = value
96
+ when 'sentry_secret'
97
+ pass = value
98
+ end
99
+ end
100
+ return user, pass
101
+ end
102
+ end
103
+ end
@@ -0,0 +1,37 @@
1
+ require 'base64'
2
+ require 'zlib'
3
+ require 'oj'
4
+
5
+ module Fluent
6
+ class TextParser
7
+ class SentryHttpParser < Parser
8
+ Plugin.register_parser('sentry_http', self)
9
+
10
+ config_param :json_parse, :bool, :default => true
11
+ config_param :field_name, :string, :default => 'message'
12
+
13
+ def initialize
14
+ super
15
+ end
16
+
17
+ def configure(conf)
18
+ super
19
+ end
20
+
21
+ def parse(text)
22
+ message = Zlib::Inflate.inflate(Base64.decode64(text))
23
+ record = Oj.load(message, :mode => :compat)
24
+
25
+ record_time = record['timestamp']
26
+ time = record_time.nil? ? Engine.now : Time.parse(record_time).to_i
27
+
28
+ record = {@field_name => message} unless @json_parse
29
+
30
+ yield time, record
31
+ rescue => e
32
+ $log.warn "parse error: #{e.message}"
33
+ yield nil, nil
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,7 @@
1
+ module Fluent
2
+ module Plugin
3
+ module SentryHttp
4
+ VERSION = '0.1.0'
5
+ end
6
+ end
7
+ end
data/test/helper.rb ADDED
@@ -0,0 +1,48 @@
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
+
15
+ require 'fileutils'
16
+ require 'fluent/log'
17
+ require 'fluent/test'
18
+
19
+ $log = Fluent::Log.new(STDOUT, Fluent::Log::LEVEL_DEBUG)
20
+
21
+ unless defined?(Test::Unit::AssertionFailedError)
22
+ class Test::Unit::AssertionFailedError < StandardError
23
+ end
24
+ end
25
+
26
+ def unused_port
27
+ s = TCPServer.open(0)
28
+ port = s.addr[1]
29
+ s.close
30
+ port
31
+ end
32
+
33
+ def ipv6_enabled?
34
+ require 'socket'
35
+
36
+ begin
37
+ TCPServer.open("::1", 0)
38
+ true
39
+ rescue
40
+ false
41
+ end
42
+ end
43
+
44
+ require 'fluent/plugin/in_sentry_http'
45
+ require 'fluent/plugin/parser_sentry_http'
46
+
47
+ class Test::Unit::TestCase
48
+ end
@@ -0,0 +1,137 @@
1
+ require 'helper'
2
+ require 'net/http'
3
+ require 'base64'
4
+ require 'zlib'
5
+ require 'oj'
6
+
7
+ class SentryHttpInputTest < Test::Unit::TestCase
8
+ def setup
9
+ Fluent::Test.setup
10
+ end
11
+
12
+ PORT = unused_port
13
+ HOST = '127.0.0.1'
14
+ APPLICATION = 999
15
+ TAG = 'from.raven'
16
+ USER = 'test_user'
17
+ PASS = 'test_pass'
18
+
19
+ CONFIG = %[
20
+ port #{PORT}
21
+ bind #{HOST}
22
+
23
+ <application #{APPLICATION}>
24
+ tag #{TAG}
25
+ user #{USER}
26
+ pass #{PASS}
27
+ </application>
28
+ ]
29
+
30
+ def create_driver(conf=CONFIG)
31
+ Fluent::Test::InputTestDriver.new(Fluent::SentryHttpInput).configure(conf)
32
+ end
33
+
34
+ def test_configure
35
+ d = create_driver
36
+ assert_equal PORT, d.instance.port
37
+ assert_equal '127.0.0.1', d.instance.bind
38
+
39
+ application = d.instance.mapping[APPLICATION.to_s]
40
+ assert_equal TAG, application['tag']
41
+ assert_equal USER, application['user']
42
+ assert_equal PASS, application['pass']
43
+ end
44
+
45
+ def test_success_request
46
+ time, record, expect_record = create_time_and_record
47
+ d = create_driver
48
+ d.expect_emit TAG, time, expect_record
49
+ d.run do
50
+ res = post_record(
51
+ APPLICATION,
52
+ create_headers(time, USER, PASS),
53
+ record_to_payload(record))
54
+ assert_equal '200', res.code
55
+ end
56
+ end
57
+
58
+ def test_not_found_request
59
+ time, record, expect_record = create_time_and_record
60
+ d = create_driver
61
+ d.run do
62
+ res = post_record(
63
+ 998,
64
+ create_headers(time, USER, PASS),
65
+ record_to_payload(record))
66
+ assert_equal '404', res.code
67
+ end
68
+ end
69
+
70
+ def test_unauthorized_request
71
+ time, record, expect_record = create_time_and_record
72
+ d = create_driver
73
+ d.run do
74
+ res = post_record(
75
+ APPLICATION,
76
+ create_headers(time, 'ham', PASS),
77
+ record_to_payload(record))
78
+ assert_equal '401', res.code
79
+
80
+ res = post_record(
81
+ APPLICATION,
82
+ create_headers(time, USER, 'egg'),
83
+ record_to_payload(record))
84
+ assert_equal '401', res.code
85
+ end
86
+ end
87
+
88
+ def test_bad_request
89
+ time, record, expect_record = create_time_and_record
90
+ d = create_driver
91
+ d.run do
92
+ res = post_record(
93
+ APPLICATION,
94
+ create_headers(time, USER, PASS),
95
+ 'spam')
96
+ assert_equal '400', res.code
97
+ end
98
+ end
99
+
100
+ private
101
+
102
+ def create_time_and_record
103
+ timestamp = '2015-07-26T09:24:30Z'
104
+ time = Time.parse(timestamp).to_i
105
+ record = create_record(timestamp)
106
+ expect_record = record.merge({'tag' => TAG, 'time' => time})
107
+ return time, record, expect_record
108
+ end
109
+
110
+ def create_headers(time, user, pass)
111
+ {
112
+ 'X-Sentry-Auth' => "Sentry sentry_timestamp=#{time}, sentry_client=raven-python/5.2.0, sentry_version=6, sentry_key=#{user}, sentry_secret=#{pass}",
113
+ 'Content-Type' => 'application/octet-stream',
114
+ 'User-Agent' => 'raven-python/5.2.0',
115
+ }
116
+ end
117
+
118
+ def create_record(timestamp)
119
+ {
120
+ 'timestamp' => timestamp,
121
+ 'message' => 'test',
122
+ }
123
+ end
124
+
125
+ def record_to_payload(record)
126
+ text = Oj.dump(record, :mode => :compat)
127
+ compressed_text = Zlib::Deflate.deflate(text)
128
+ Base64.encode64(compressed_text)
129
+ end
130
+
131
+ def post_record(application, headers, payload)
132
+ http = Net::HTTP.new(HOST, PORT)
133
+ req = Net::HTTP::Post.new("/api/#{application}/store/", headers)
134
+ req.body = payload
135
+ http.request(req)
136
+ end
137
+ end
metadata ADDED
@@ -0,0 +1,133 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: fluent-plugin-sentry-http
3
+ version: !ruby/object:Gem::Version
4
+ prerelease: false
5
+ segments:
6
+ - 0
7
+ - 1
8
+ - 0
9
+ version: 0.1.0
10
+ platform: ruby
11
+ authors:
12
+ - IKUTA Masahito
13
+ autorequire:
14
+ bindir: bin
15
+ cert_chain: []
16
+
17
+ date: 2015-07-26 00:00:00 +09:00
18
+ default_executable:
19
+ dependencies:
20
+ - !ruby/object:Gem::Dependency
21
+ prerelease: false
22
+ type: :development
23
+ name: rake
24
+ version_requirements: &id001 !ruby/object:Gem::Requirement
25
+ requirements:
26
+ - - ~>
27
+ - !ruby/object:Gem::Version
28
+ segments:
29
+ - 10
30
+ - 0
31
+ version: "10.0"
32
+ requirement: *id001
33
+ - !ruby/object:Gem::Dependency
34
+ prerelease: false
35
+ type: :development
36
+ name: test-unit
37
+ version_requirements: &id002 !ruby/object:Gem::Requirement
38
+ requirements:
39
+ - - ~>
40
+ - !ruby/object:Gem::Version
41
+ segments:
42
+ - 3
43
+ - 0
44
+ version: "3.0"
45
+ - - ~>
46
+ - !ruby/object:Gem::Version
47
+ segments:
48
+ - 3
49
+ - 1
50
+ version: "3.1"
51
+ requirement: *id002
52
+ - !ruby/object:Gem::Dependency
53
+ prerelease: false
54
+ type: :runtime
55
+ name: fluentd
56
+ version_requirements: &id003 !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - ">="
59
+ - !ruby/object:Gem::Version
60
+ segments:
61
+ - 0
62
+ - 10
63
+ - 55
64
+ version: 0.10.55
65
+ requirement: *id003
66
+ - !ruby/object:Gem::Dependency
67
+ prerelease: false
68
+ type: :runtime
69
+ name: oj
70
+ version_requirements: &id004 !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - ">="
73
+ - !ruby/object:Gem::Version
74
+ segments:
75
+ - 1
76
+ - 4
77
+ version: "1.4"
78
+ requirement: *id004
79
+ description: Fluentd input plugin that receive exceptions from the Sentry clients(Raven).
80
+ email:
81
+ - cooldaemon@gmail.com
82
+ executables: []
83
+
84
+ extensions: []
85
+
86
+ extra_rdoc_files: []
87
+
88
+ files:
89
+ - .gitignore
90
+ - .travis.yml
91
+ - Gemfile
92
+ - LICENSE.txt
93
+ - README.md
94
+ - Rakefile
95
+ - fluent-plugin-sentry-http.gemspec
96
+ - lib/fluent/plugin/in_sentry_http.rb
97
+ - lib/fluent/plugin/parser_sentry_http.rb
98
+ - lib/fluent/plugin/sentry_http/version.rb
99
+ - test/helper.rb
100
+ - test/plugin/test_in_sentry_http.rb
101
+ has_rdoc: true
102
+ homepage: http://github.com/cooldaemon/fluent-plugin-sentry-http
103
+ licenses:
104
+ - APLv2
105
+ post_install_message:
106
+ rdoc_options: []
107
+
108
+ require_paths:
109
+ - lib
110
+ required_ruby_version: !ruby/object:Gem::Requirement
111
+ requirements:
112
+ - - ">="
113
+ - !ruby/object:Gem::Version
114
+ segments:
115
+ - 0
116
+ version: "0"
117
+ required_rubygems_version: !ruby/object:Gem::Requirement
118
+ requirements:
119
+ - - ">="
120
+ - !ruby/object:Gem::Version
121
+ segments:
122
+ - 0
123
+ version: "0"
124
+ requirements: []
125
+
126
+ rubyforge_project:
127
+ rubygems_version: 1.3.6
128
+ signing_key:
129
+ specification_version: 3
130
+ summary: Fluentd input plugin that receive exceptions from the Sentry clients(Raven).
131
+ test_files:
132
+ - test/helper.rb
133
+ - test/plugin/test_in_sentry_http.rb