rails-graylogger 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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 42cf591e1b56d39e146aa353a2ed0f3bbb0bd93e
4
+ data.tar.gz: 9a7779e5a53422ea3e6e6fc946b9c58c8df3490c
5
+ SHA512:
6
+ metadata.gz: 9fc47c8907de6a7d46b567d191936d627790d91125f408d84b8ccca9a64605c0f5cd5623fa86cee00fef17c5330aedd7740d5a92d9c241f7379f4f6d91b21977
7
+ data.tar.gz: 88b6c0dadbe9e8f81de79fbb6a170c063f9114dd063c9733aa8a4a1cda232655fffaea5ef1bef20e8b5de998d7f78c876402646f242599bc4e4e4ae0f4fe7f60
data/.gitignore ADDED
@@ -0,0 +1,5 @@
1
+ *.gem
2
+ .bundle
3
+ pkg
4
+ .yardoc
5
+ test.log
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source "https://rubygems.org"
2
+
3
+ gemspec
data/Gemfile.lock ADDED
@@ -0,0 +1,48 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ rails-graylogger (0.1.0)
5
+ activesupport (~> 4)
6
+ gelf (~> 1.4.0)
7
+ request_store (~> 1.0)
8
+
9
+ GEM
10
+ remote: https://rubygems.org/
11
+ specs:
12
+ activesupport (4.2.0)
13
+ i18n (~> 0.7)
14
+ json (~> 1.7, >= 1.7.7)
15
+ minitest (~> 5.1)
16
+ thread_safe (~> 0.3, >= 0.3.4)
17
+ tzinfo (~> 1.1)
18
+ diff-lcs (1.2.5)
19
+ gelf (1.4.0)
20
+ json
21
+ i18n (0.7.0)
22
+ json (1.8.2)
23
+ minitest (5.5.1)
24
+ rake (10.4.2)
25
+ request_store (1.1.0)
26
+ rspec (3.1.0)
27
+ rspec-core (~> 3.1.0)
28
+ rspec-expectations (~> 3.1.0)
29
+ rspec-mocks (~> 3.1.0)
30
+ rspec-core (3.1.7)
31
+ rspec-support (~> 3.1.0)
32
+ rspec-expectations (3.1.2)
33
+ diff-lcs (>= 1.2.0, < 2.0)
34
+ rspec-support (~> 3.1.0)
35
+ rspec-mocks (3.1.3)
36
+ rspec-support (~> 3.1.0)
37
+ rspec-support (3.1.2)
38
+ thread_safe (0.3.4)
39
+ tzinfo (1.2.2)
40
+ thread_safe (~> 0.1)
41
+
42
+ PLATFORMS
43
+ ruby
44
+
45
+ DEPENDENCIES
46
+ rails-graylogger!
47
+ rake
48
+ rspec (~> 3)
data/README.md ADDED
@@ -0,0 +1,34 @@
1
+ # rails-graylogger
2
+ Rails logs to Graylog2
3
+
4
+ # Configuring Middleware
5
+ Add the following to config/application.rb:
6
+
7
+ ```
8
+ config.middleware.insert_before Rack::Runtime, RailsGraylogger::Middleware
9
+ config.after_initialize do
10
+ config.logger = Rails.logger = RailsGraylogger::Logger.new(Rails.logger)
11
+ end
12
+ ```
13
+
14
+ # Watching output without graylog2
15
+ One way is to use logstash to listen for gelf input and output it to a file
16
+
17
+ Config:
18
+
19
+ ```
20
+ input {
21
+ gelf {
22
+ }
23
+ }
24
+ output {
25
+ file {
26
+ path => "/tmp/test.log"
27
+ }
28
+ }
29
+ ```
30
+
31
+ ```
32
+ brew install logstash
33
+ logstash -f <path_to_config>
34
+ ```
data/Rakefile ADDED
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env rake
2
+ require "bundler/gem_tasks"
3
+
4
+ require 'rspec/core/rake_task'
5
+ RSpec::Core::RakeTask.new(:spec)
6
+
7
+ task default: :spec
@@ -0,0 +1,37 @@
1
+ module RailsGraylogger
2
+ class LogSubscriber < ::ActiveSupport::LogSubscriber
3
+
4
+ def process_action(event)
5
+ payload = event.payload
6
+ payload[:path] = URI(payload[:path]).path if payload[:path]
7
+ payload[:ipaddress] = RequestStore.store[:ipaddress]
8
+ payload[:duration] = event.duration.round
9
+ payload[:status] = status_from_exception(payload[:exception]) unless payload[:status].present?
10
+
11
+ message = RailsGraylogger::Message.from_event_payload(payload)
12
+ message.tags = RailsGraylogger::Logger.request_tags
13
+ message.full_message = buffered_messages
14
+
15
+ RailsGraylogger::Notifier.notify!(message.to_hash)
16
+ rescue => ex
17
+ Rails.logger.error "Exception in RailsGraylogger: #{ex.class}: #{ex.message}"
18
+ end
19
+
20
+ def buffered_messages
21
+ unless RailsGraylogger::Logger.request_buffer.blank?
22
+ RailsGraylogger::Logger.request_buffer.map{ |item| item[:short_message] unless item[:short_message].blank? }.compact.join("\n")
23
+ else
24
+ []
25
+ end
26
+ end
27
+
28
+ def status_from_exception(exception)
29
+ if exception.present?
30
+ exception_class_name = exception.first
31
+ ActionDispatch::ExceptionWrapper.status_code_for_exception(exception_class_name)
32
+ end
33
+ end
34
+ end
35
+ end
36
+
37
+ RailsGraylogger::LogSubscriber.attach_to :action_controller
@@ -0,0 +1,59 @@
1
+ module RailsGraylogger
2
+ class Logger
3
+ FIELD_KEY_REGEXP = /^[\w\.\-]*$/
4
+
5
+ def initialize(logger)
6
+ @logger = logger
7
+ end
8
+
9
+ class << self
10
+ def initialize_log_buffer
11
+ RequestStore.store[:graylog2_buffer] = []
12
+ end
13
+
14
+ def request_tags
15
+ RequestStore.store[:graylog2_tags] ||= []
16
+ end
17
+
18
+ def request_buffer
19
+ RequestStore.store[:graylog2_buffer]
20
+ end
21
+ end
22
+
23
+ def tagged(*new_tags, &block)
24
+ new_tags = Array.wrap(new_tags).flatten.reject(&:blank?)
25
+ self.class.request_tags << new_tags - ( self.class.request_tags & new_tags )
26
+ @logger.send("tagged", *new_tags, &block)
27
+ end
28
+
29
+ private
30
+
31
+ def method_missing(method, *args, &block)
32
+ if [:add, :info, :debug, :warn, :error, :fatal].include?(method) && !args[0].nil?
33
+ if args.size == 1 && args[0].is_a?(String)
34
+ hash = { short_message: args[0] }
35
+ else
36
+ return if args.blank?
37
+ hash = { short_message: args.compact.map(&:to_s).join("\n") }
38
+ end
39
+
40
+ unless self.class.request_buffer.nil?
41
+ self.class.request_buffer << hash
42
+ else
43
+ notify!(method, hash)
44
+ end
45
+ end
46
+ @logger.send(method, *args, &block)
47
+ end
48
+
49
+ def notify!(method, payload)
50
+ message = RailsGraylogger::Message.new({
51
+ level: "GELF::Levels::#{method.upcase}".constantize,
52
+ short_message: payload.delete(:short_message)
53
+ })
54
+ message.process_extra_fields(payload)
55
+ message.tags = self.class.request_tags unless self.class.request_tags.blank?
56
+ RailsGraylogger::Notifier.notify!(message.to_hash)
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,74 @@
1
+ module RailsGraylogger
2
+ class Message
3
+ FIELD_KEY_REGEXP = /^[\w\.\-]*$/
4
+
5
+ attr_accessor :level, :full_message, :short_message, :extra_fields
6
+
7
+ def initialize(opts = {})
8
+ @level = opts[:level] || GELF::Levels::INFO
9
+ @short_message = opts[:short_message] || ""
10
+ @full_message = opts[:full_message] || ""
11
+ @extra_fields = {}
12
+ end
13
+
14
+ class << self
15
+ def from_event_payload(payload)
16
+ message = self.new
17
+ message.process_event_payload(payload)
18
+ message
19
+ end
20
+ end
21
+
22
+ def process_event_payload(payload)
23
+ self.level = level_from_status(payload[:status])
24
+ self.short_message = "#{payload[:controller]}##{payload[:action]} #{payload[:method]} \"#{payload[:path]}\" from #{payload[:ipaddress]}"
25
+ process_extra_fields(payload)
26
+ end
27
+
28
+ def tags=(tags)
29
+ raise ArgumentError.new("Not an array: #{tags.inspect}") unless tags.is_a?(Array)
30
+ self.extra_fields[:_tags] = tags.join(" ")
31
+ end
32
+
33
+ def to_hash
34
+ {
35
+ level: level,
36
+ short_message: short_message
37
+ }.tap do |hash|
38
+ hash[:full_message] = short_message + "\n" + full_message unless full_message.blank?
39
+ end.merge(extra_fields)
40
+ end
41
+
42
+ def process_extra_fields(payload)
43
+ process_exception_data(payload.delete(:exception)) if payload[:exception]
44
+
45
+ payload.each do |key, value|
46
+ @extra_fields["_#{key}"] = formatted_value(value) if valid_key?(key)
47
+ end
48
+ end
49
+
50
+ private
51
+
52
+ def formatted_value(value)
53
+ if value.is_a?(Numeric)
54
+ value
55
+ else
56
+ value.to_s
57
+ end
58
+ end
59
+
60
+ def valid_key?(key)
61
+ key.match(FIELD_KEY_REGEXP)
62
+ end
63
+
64
+ def level_from_status(status)
65
+ status < 400 ? GELF::Levels::INFO : GELF::Levels::ERROR
66
+ end
67
+
68
+ def process_exception_data(exception)
69
+ exception.each do |key, value|
70
+ @extra_fields["_exception_#{key}"] = value.to_s if valid_key?(key)
71
+ end
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,18 @@
1
+ module RailsGraylogger
2
+ class Middleware
3
+ def initialize(app)
4
+ @app = app
5
+ end
6
+
7
+ def call(env)
8
+ RailsGraylogger::Logger.initialize_log_buffer
9
+ logger = Rails.logger
10
+ request = Rack::Request.new(env)
11
+ RequestStore.store[:ipaddress] = request.ip
12
+
13
+ status, headers, body = @app.call(env)
14
+
15
+ [status, headers, body]
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,35 @@
1
+ module RailsGraylogger
2
+ class Notifier
3
+ class << self
4
+ def notify!(gelf_message)
5
+ @@notifier ||= self.new
6
+ @@notifier.notify!(gelf_message)
7
+ end
8
+ end
9
+
10
+ def initialize
11
+ @gelf_notifier = GELF::Notifier.new(remote_host, remote_port, 1420, facility: facility, host: sender_host)
12
+ end
13
+
14
+ def remote_host
15
+ ENV["GRAYLOG_HOST"] || "127.0.0.1"
16
+ end
17
+
18
+ def remote_port
19
+ ENV["GRAYLOG_PORT"] || 12201
20
+ end
21
+
22
+ def facility
23
+ ENV["GRAYLOG_FACILITY"] || "Rails"
24
+ end
25
+
26
+ def sender_host
27
+ Socket.gethostname.split(".").first
28
+ end
29
+
30
+ def notify!(gelf_message)
31
+ return if Rails.env == 'test'
32
+ @gelf_notifier.notify!(gelf_message.merge(timestamp: Time.now.utc.to_f))
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,3 @@
1
+ module RailsGraylogger
2
+ VERSION = '0.1.0'
3
+ end
@@ -0,0 +1,7 @@
1
+ require 'gelf'
2
+ require 'request_store'
3
+ require 'rails-graylogger/middleware.rb'
4
+ require 'rails-graylogger/notifier.rb'
5
+ require 'rails-graylogger/logger.rb'
6
+ require 'rails-graylogger/message.rb'
7
+ require 'rails-graylogger/log_subscriber.rb'
@@ -0,0 +1,25 @@
1
+ # -*- encoding: utf-8 -*-
2
+ require File.expand_path('../lib/rails-graylogger/version', __FILE__)
3
+
4
+ Gem::Specification.new do |s|
5
+ s.name = "rails-graylogger"
6
+ s.version = RailsGraylogger::VERSION
7
+ s.platform = Gem::Platform::RUBY
8
+ s.authors = ["Tuomas Silen"]
9
+ s.email = ["tuomas@flowdock.com"]
10
+ s.homepage = "https://github.com/flowdock/rails-graylogger"
11
+ s.summary = "Push Rails logs also to Graylog2"
12
+ s.description = s.summary
13
+
14
+ s.add_dependency 'gelf', '~> 1.4.0'
15
+ s.add_dependency 'activesupport', '~> 4'
16
+ s.add_dependency 'request_store', '~> 1.0'
17
+
18
+ s.add_development_dependency 'rspec', '~> 3'
19
+ s.add_development_dependency 'rake'
20
+
21
+ s.files = `git ls-files`.split("\n")
22
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
23
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
24
+ s.require_paths = ["lib"]
25
+ end
@@ -0,0 +1,49 @@
1
+ require "spec_helper"
2
+
3
+ describe RailsGraylogger::Logger do
4
+ before :each do
5
+ RequestStore.clear!
6
+ @logger = RailsGraylogger::Logger.new(ActiveSupport::TaggedLogging.new(Logger.new("test.log")))
7
+ end
8
+
9
+ it "sends log lines to Graylog server" do
10
+ expect(RailsGraylogger::Notifier).to receive(:notify!).with(level: GELF::Levels::INFO, short_message: "Foobar").once
11
+ @logger.info("Foobar")
12
+ end
13
+
14
+ it "handles tagged logging with nested tags" do
15
+ expect(RailsGraylogger::Notifier).to receive(:notify!).with(level: GELF::Levels::INFO, short_message: "Test", _tags: "Foo Bar").once
16
+ @logger.tagged("Foo") do
17
+ @logger.tagged("Bar") do
18
+ @logger.info("Test")
19
+ end
20
+ end
21
+ end
22
+
23
+ it "handles tagged logging with multiple log lines" do
24
+ expect(RailsGraylogger::Notifier).to receive(:notify!).with(_tags: "Foobar", short_message: "Line1", level: GELF::Levels::INFO).once
25
+ expect(RailsGraylogger::Notifier).to receive(:notify!).with(_tags: "Foobar", short_message: "Line2", level: GELF::Levels::INFO).once
26
+ @logger.tagged("Foobar") do
27
+ @logger.info("Line1")
28
+ @logger.info("Line2")
29
+ end
30
+ end
31
+
32
+ it "handles passing a hash as argument" do
33
+ hash = { foo: "test", bar: "test" }
34
+ expect(RailsGraylogger::Notifier).to receive(:notify!).with(level: GELF::Levels::INFO, short_message: "{:foo=>\"test\", :bar=>\"test\"}", _tags: "Foobar").once
35
+ @logger.tagged("Foobar") do
36
+ @logger.info(hash)
37
+ end
38
+ end
39
+
40
+ it "should buffer logging output when request store is initialized" do
41
+ allow(RailsGraylogger::Notifier).to receive(:notify!)
42
+ RailsGraylogger::Logger.initialize_log_buffer
43
+ expect(RailsGraylogger::Logger.request_buffer.size).to eq(0)
44
+ @logger.info("Foobar")
45
+ expect(RailsGraylogger::Notifier).not_to have_received(:notify!)
46
+ expect(RailsGraylogger::Logger.request_buffer.size).to eq(1)
47
+ expect(RailsGraylogger::Logger.request_buffer[0]).to eq({short_message: "Foobar"})
48
+ end
49
+ end
@@ -0,0 +1,24 @@
1
+ require "spec_helper"
2
+
3
+ describe RailsGraylogger::Message do
4
+ describe "#process_extra_fields" do
5
+ before(:each) do
6
+ @msg = RailsGraylogger::Message.new
7
+ end
8
+
9
+ it "should add undescrore before field names" do
10
+ @msg.process_extra_fields({ test: "abc" })
11
+ expect(@msg.extra_fields).to eq({ "_test" => "abc" })
12
+ end
13
+
14
+ it "should keep numeric values" do
15
+ @msg.process_extra_fields({ test: 123 })
16
+ expect(@msg.extra_fields).to eq({ "_test" => 123 })
17
+ end
18
+
19
+ it "should convert hash to string" do
20
+ @msg.process_extra_fields({ test: { foo: "bar" } })
21
+ expect(@msg.extra_fields).to eq({ "_test" => "{:foo=>\"bar\"}" })
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,29 @@
1
+ ENV["RAILS_ENV"] = "test"
2
+ require "active_support"
3
+ require "active_support/inflector"
4
+ require "active_support/core_ext"
5
+ require File.expand_path("../../lib/rails-graylogger", __FILE__)
6
+
7
+ #require "rspec/rails"
8
+
9
+ RSpec.configure do |config|
10
+ config.filter_run_excluding slow: true unless ENV["CI"] == "true"
11
+
12
+ # Print the 10 slowest examples and example groups at the
13
+ # end of the spec run, to help surface which specs are running
14
+ # particularly slow.
15
+ config.profile_examples = 10
16
+
17
+ # Randomise spec order
18
+ # config.order = :random
19
+ # Kernel.srand config.seed
20
+
21
+ config.expect_with :rspec do |expectations|
22
+ expectations.syntax = :expect
23
+ end
24
+
25
+ config.mock_with :rspec do |mocks|
26
+ mocks.syntax = :expect
27
+ mocks.verify_partial_doubles = false
28
+ end
29
+ end
metadata ADDED
@@ -0,0 +1,132 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: rails-graylogger
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Tuomas Silen
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2015-02-11 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: gelf
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: 1.4.0
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: 1.4.0
27
+ - !ruby/object:Gem::Dependency
28
+ name: activesupport
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '4'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '4'
41
+ - !ruby/object:Gem::Dependency
42
+ name: request_store
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '1.0'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '1.0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rspec
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '3'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '3'
69
+ - !ruby/object:Gem::Dependency
70
+ name: rake
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ description: Push Rails logs also to Graylog2
84
+ email:
85
+ - tuomas@flowdock.com
86
+ executables: []
87
+ extensions: []
88
+ extra_rdoc_files: []
89
+ files:
90
+ - ".gitignore"
91
+ - Gemfile
92
+ - Gemfile.lock
93
+ - README.md
94
+ - Rakefile
95
+ - lib/rails-graylogger.rb
96
+ - lib/rails-graylogger/log_subscriber.rb
97
+ - lib/rails-graylogger/logger.rb
98
+ - lib/rails-graylogger/message.rb
99
+ - lib/rails-graylogger/middleware.rb
100
+ - lib/rails-graylogger/notifier.rb
101
+ - lib/rails-graylogger/version.rb
102
+ - rails-graylogger.gemspec
103
+ - spec/rails-graylogger/logger_spec.rb
104
+ - spec/rails-graylogger/message_spec.rb
105
+ - spec/spec_helper.rb
106
+ homepage: https://github.com/flowdock/rails-graylogger
107
+ licenses: []
108
+ metadata: {}
109
+ post_install_message:
110
+ rdoc_options: []
111
+ require_paths:
112
+ - lib
113
+ required_ruby_version: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - ">="
116
+ - !ruby/object:Gem::Version
117
+ version: '0'
118
+ required_rubygems_version: !ruby/object:Gem::Requirement
119
+ requirements:
120
+ - - ">="
121
+ - !ruby/object:Gem::Version
122
+ version: '0'
123
+ requirements: []
124
+ rubyforge_project:
125
+ rubygems_version: 2.4.5
126
+ signing_key:
127
+ specification_version: 4
128
+ summary: Push Rails logs also to Graylog2
129
+ test_files:
130
+ - spec/rails-graylogger/logger_spec.rb
131
+ - spec/rails-graylogger/message_spec.rb
132
+ - spec/spec_helper.rb