loggerator 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: b09044e8590344b2433a544b15a853d63e92d06b
4
+ data.tar.gz: 1fc9edb37658480a85d314177440e4dd79d568c8
5
+ SHA512:
6
+ metadata.gz: baf7c00883dcfec5e32f33feeabb28d9ecf541a0cfe3c72f503b62740d18d38f2465c71dd676da26d7056c33acad54eac304e5274a61f731037787d8f59237e2
7
+ data.tar.gz: f5912098321f92a9d449e3029a53cc3c33609531e5c0ea5faf02ee6f8793bb75a5fc742689056fe6ec6c247371e82d7149b26b304185de5fca2662d96079d401
@@ -0,0 +1,17 @@
1
+ module Loggerator
2
+ class LogGenerator < Rails::Generators::Base
3
+
4
+ desc "Creates an initializer for Loggerator logs at config/initializer/log.rb"
5
+ class_option :a, banner: "APP_NAME", desc: "Specify APP_NAME instead of the one defined by Rails"
6
+ source_root File.expand_path("../../templates", __FILE__)
7
+
8
+ def create_config_file
9
+ template "log.rb.erb", "config/initializers/log.rb"
10
+ end
11
+
12
+ private
13
+ def app_name
14
+ options[:a] || Rails.root.basename
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,4 @@
1
+ Loggerator.default_context = { app: "<%= app_name %>" }
2
+ <% if defined?(Loggerator::Metrics) -%>
3
+ Loggerator::Metrics.name = "<%= app_name %>"
4
+ <% end -%>
@@ -0,0 +1 @@
1
+ require 'loggerator/loggerator'
@@ -0,0 +1,146 @@
1
+ require_relative 'request_store'
2
+ require_relative 'middleware'
3
+
4
+ module Loggerator
5
+ extend self
6
+
7
+ def self.included(mod)
8
+ mod.extend self
9
+ end
10
+
11
+ def log(data, &block)
12
+ log_to_stream(stdout, merge_log_contexts(data), &block)
13
+ end
14
+
15
+ def log_error(e, data = {})
16
+ exception_id = e.object_id
17
+
18
+ # Log backtrace in reverse order for easier digestion.
19
+ if e.backtrace
20
+ e.backtrace.reverse.each do |backtrace|
21
+ log_to_stream(stderr, merge_log_contexts(
22
+ exception_id: exception_id,
23
+ backtrace: backtrace
24
+ ))
25
+ end
26
+ end
27
+
28
+ # then log the exception message last so that it's as close to the end of
29
+ # a log trace as possible
30
+ data.merge!(
31
+ exception: true,
32
+ class: e.class.name,
33
+ message: e.message,
34
+ exception_id: exception_id
35
+ )
36
+
37
+ data[:status] = e.status if e.respond_to?(:status)
38
+
39
+ log_to_stream(stderr, merge_log_contexts(data))
40
+ end
41
+
42
+ def log_context(data, &block)
43
+ old = local_context
44
+ self.local_context = old.merge(data)
45
+ res = block.call
46
+ ensure
47
+ self.local_context = old
48
+ res
49
+ end
50
+
51
+ def default_context=(default_context)
52
+ @@default_context = default_context
53
+ end
54
+
55
+ def default_context
56
+ @@default_context ||= {}
57
+ end
58
+
59
+ def stdout=(stream)
60
+ @@stdout = stream
61
+ end
62
+
63
+ def stdout
64
+ @@stdout ||= $stdout
65
+ end
66
+
67
+ def stderr=(stream)
68
+ @@stderr = stream
69
+ end
70
+
71
+ def stderr
72
+ @@stderr ||= $stderr
73
+ end
74
+
75
+ private
76
+ def merge_log_contexts(data)
77
+ default_context.merge(request_context.merge(local_context.merge(data)))
78
+ end
79
+
80
+ def local_context
81
+ RequestStore.store[:local_context] ||= {}
82
+ end
83
+
84
+ def local_context=(h)
85
+ RequestStore.store[:local_context] = h
86
+ end
87
+
88
+ def request_context
89
+ RequestStore.store[:request_context] || {}
90
+ end
91
+
92
+ def log_to_stream(stream, data, &block)
93
+ unless block
94
+ str = unparse(data)
95
+ stream.print(str + "\n")
96
+ else
97
+ data = data.dup
98
+ start = Time.now
99
+ log_to_stream(stream, data.merge(at: 'start'))
100
+ begin
101
+ res = yield
102
+
103
+ log_to_stream(stream, data.merge(
104
+ at: 'finish', elapsed: (Time.now - start).to_f))
105
+ res
106
+ rescue
107
+ log_to_stream(stream, data.merge(
108
+ at: 'exception', elapsed: (Time.now - start).to_f))
109
+ raise $!
110
+ end
111
+ end
112
+ end
113
+
114
+ def unparse(attrs)
115
+ attrs.map { |k, v| unparse_pair(k, v) }.compact.join(" ")
116
+ end
117
+
118
+ def unparse_pair(k, v)
119
+ v = v.call if v.is_a?(Proc)
120
+ # only quote strings if they include whitespace
121
+ if v == nil
122
+ nil
123
+ elsif v == true
124
+ k
125
+ elsif v.is_a?(Float)
126
+ "#{k}=#{format("%.3f", v)}"
127
+ elsif v.is_a?(String) && v =~ /\s/
128
+ quote_string(k, v)
129
+ elsif v.is_a?(Time)
130
+ "#{k}=#{v.iso8601}"
131
+ else
132
+ "#{k}=#{v}"
133
+ end
134
+ end
135
+
136
+ def quote_string(k, v)
137
+ # try to find a quote style that fits
138
+ if !v.include?('"')
139
+ %{#{k}="#{v}"}
140
+ elsif !v.include?("'")
141
+ %{#{k}='#{v}'}
142
+ else
143
+ %{#{k}="#{v.gsub(/"/, '\\"')}"}
144
+ end
145
+ end
146
+ end
@@ -0,0 +1,41 @@
1
+ require_relative 'loggerator'
2
+
3
+ module Loggerator
4
+ module Metrics
5
+ include Loggerator
6
+ extend self
7
+
8
+ @@metrics_name = 'loggerator'
9
+
10
+ def name=(name)
11
+ @@metrics_name = name
12
+ end
13
+
14
+ def name
15
+ @@metrics_name
16
+ end
17
+
18
+ def count(key, value=1)
19
+ log("count##{name}.#{key}" => value)
20
+ end
21
+
22
+ def sample(key, value)
23
+ log("sample##{name}.#{key}" => value)
24
+ end
25
+
26
+ def unique(key, value)
27
+ log("unique##{name}.#{key}" => value)
28
+ end
29
+
30
+ def measure(key, value, units='s')
31
+ log("measure##{name}.#{key}" => "#{value}#{units}")
32
+ end
33
+
34
+ end
35
+
36
+ # included Metrics shortcut
37
+ def m; Metrics; end
38
+ end
39
+
40
+ # simple alias if its not already being used
41
+ Metrics = Loggerator::Metrics unless defined?(Metrics)
@@ -0,0 +1,7 @@
1
+ require_relative 'middleware/request_store'
2
+ require_relative 'middleware/request_id'
3
+
4
+ module Loggerator
5
+ module Middleware; end
6
+ end
7
+
@@ -0,0 +1,55 @@
1
+ module Loggerator::Middleware
2
+ class RequestID
3
+ # note that this pattern supports either a full UUID, or a "squashed" UUID
4
+ # like the kind Hermes sends:
5
+ #
6
+ # full: 01234567-89ab-cdef-0123-456789abcdef
7
+ # squashed: 0123456789abcdef0123456789abcdef
8
+ #
9
+ UUID_PATTERN =
10
+ /\A[a-f0-9]{8}-?[a-f0-9]{4}-?[a-f0-9]{4}-?[a-f0-9]{4}-?[a-f0-9]{12}\Z/
11
+
12
+ def initialize(app)
13
+ @app = app
14
+ end
15
+
16
+ def call(env)
17
+ request_ids = [SecureRandom.uuid] + extract_request_ids(env)
18
+
19
+ # make ID of the request accessible to consumers down the stack
20
+ env["REQUEST_ID"] = request_ids[0]
21
+
22
+ # Extract request IDs from incoming headers as well. Can be used for
23
+ # identifying a request across a number of components in SOA.
24
+ env["REQUEST_IDS"] = request_ids
25
+
26
+ status, headers, response = @app.call(env)
27
+
28
+ # tag all responses with a request ID
29
+ headers["Request-Id"] = request_ids[0]
30
+
31
+ [status, headers, response]
32
+ end
33
+
34
+ private
35
+ def extract_request_ids(env)
36
+ request_ids = raw_request_ids(env)
37
+ request_ids.map! { |id| id.strip }
38
+ request_ids.select! { |id| id =~ UUID_PATTERN }
39
+ request_ids
40
+ end
41
+
42
+ def raw_request_ids(env)
43
+ # We had a little disagreement around the inception of the Request-Id
44
+ # field as to whether it should be prefixed with `X-` or not. API went
45
+ # with no prefix, but Hermes went with one. Support both formats on
46
+ # input.
47
+ %w(HTTP_REQUEST_ID HTTP_X_REQUEST_ID).inject([]) do |request_ids, key|
48
+ if ids = env[key]
49
+ request_ids += ids.split(",")
50
+ end
51
+ request_ids
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,14 @@
1
+ module Loggerator::Middleware
2
+ class RequestStore
3
+ def initialize(app, options={})
4
+ @app = app
5
+ @store = options[:store] || Loggerator::RequestStore
6
+ end
7
+
8
+ def call(env)
9
+ @store.clear!
10
+ @store.seed(env)
11
+ @app.call(env)
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,22 @@
1
+ require_relative 'loggerator'
2
+
3
+ module Loggerator
4
+ module Namespace
5
+ include Loggerator
6
+
7
+ def log(data={}, &blk)
8
+ log_namespace!
9
+ super
10
+ end
11
+
12
+ def log_error(e, data={})
13
+ log_namespace!
14
+ super
15
+ end
16
+
17
+ private
18
+ def log_namespace!
19
+ self.local_context = { ns: self.class.name }
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,22 @@
1
+ require_relative "loggerator"
2
+
3
+ module Loggerator
4
+ class Railtie < Rails::Railtie
5
+
6
+ config.before_configuration do
7
+ Rails.application.middleware.insert_after ActionDispatch::RequestId, Loggerator::Middleware::RequestStore
8
+ Rails.application.middleware.swap ActionDispatch::RequestId, Loggerator::Middleware::RequestID
9
+ end
10
+
11
+ config.before_initialize do
12
+ [ ActionView::Base,
13
+ ActiveRecord::Base,
14
+ ActionMailer::Base,
15
+ ActionController::Base ].each do |c|
16
+
17
+ c.include Loggerator
18
+ end
19
+ end
20
+
21
+ end
22
+ end
@@ -0,0 +1,24 @@
1
+ module Loggerator
2
+ module RequestStore
3
+ class << self
4
+ def clear!
5
+ Thread.current[:request_store] = {}
6
+ end
7
+
8
+ def seed(env)
9
+ store[:request_id] =
10
+ env["REQUEST_IDS"] ? env["REQUEST_IDS"].join(",") : nil
11
+
12
+ # a global context that evolves over the lifetime of the request, and is
13
+ # used to tag all log messages that it produces
14
+ store[:request_context] = {
15
+ request_id: store[:request_id]
16
+ }
17
+ end
18
+
19
+ def store
20
+ Thread.current[:request_store] ||= {}
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,32 @@
1
+ module Loggerator
2
+ alias_method :log_on, :log
3
+ alias_method :log_error_on, :log_error
4
+
5
+ def log_off(data, &block)
6
+ block.call if block
7
+ end
8
+
9
+ def log_error_off(e, data={}, &block)
10
+ block.call if block
11
+ end
12
+
13
+ class << self
14
+ @@log_switch = true
15
+
16
+ def turn_log(on_or_off)
17
+ return unless %i[on off].include?(on_or_off.to_sym)
18
+ alias_method :log, :"log_#{on_or_off}"
19
+ alias_method :log_error, :"log_error_#{on_or_off}"
20
+ @@log_switch = on_or_off.to_sym == :on
21
+ end
22
+
23
+ def log?
24
+ @@log_switch
25
+ end
26
+ end
27
+
28
+ end
29
+
30
+ unless ENV.has_key?("TEST_LOGS")
31
+ Loggerator.turn_log(:off)
32
+ end
@@ -0,0 +1,33 @@
1
+ require 'rack/test'
2
+ require 'minitest/mock'
3
+ require 'minitest/autorun'
4
+ require 'logger'
5
+
6
+ require_relative '../../../lib/loggerator'
7
+
8
+ class Loggerator::Middleware::TestRequestID < Minitest::Test
9
+ include Rack::Test::Methods
10
+
11
+ def app
12
+ Rack::Builder.new do
13
+ use Rack::Lint
14
+ use Loggerator::Middleware::RequestID
15
+
16
+ run ->(env) { [ 200, { }, [ 'hi' ] ] }
17
+ end
18
+ end
19
+
20
+ def test_sets_request_id
21
+ get '/'
22
+
23
+ assert_match ::Loggerator::Middleware::RequestID::UUID_PATTERN,
24
+ last_request.env['REQUEST_ID']
25
+ end
26
+
27
+ def test_sets_request_ids
28
+ get '/'
29
+
30
+ assert_match ::Loggerator::Middleware::RequestID::UUID_PATTERN,
31
+ last_request.env['REQUEST_IDS'].first
32
+ end
33
+ end
@@ -0,0 +1,40 @@
1
+ require 'rack/test'
2
+ require 'minitest/mock'
3
+ require 'minitest/autorun'
4
+ require 'logger'
5
+
6
+ require_relative '../../../lib/loggerator'
7
+
8
+ class Loggerator::Middleware::TestRequestStore < Minitest::Test
9
+ include Rack::Test::Methods
10
+
11
+ def app
12
+ Rack::Builder.new do
13
+ use Rack::Lint
14
+ use Loggerator::Middleware::RequestStore
15
+
16
+ run ->(env) { [ 200, { }, [ 'hi' ] ] }
17
+ end
18
+ end
19
+
20
+ def test_clears_the_store
21
+ Thread.current[:request_store] = { something_added_before: 'bar' }
22
+
23
+ get '/'
24
+
25
+ assert_nil Thread.current[:request_store][:something_added_before]
26
+ end
27
+
28
+ def test_seeds_the_store
29
+ Thread.current[:request_store] = {}
30
+
31
+ get '/'
32
+
33
+ assert_equal Thread.current[:request_store], {
34
+ request_id: nil,
35
+ request_context: {
36
+ request_id: nil
37
+ }
38
+ }
39
+ end
40
+ end
@@ -0,0 +1,35 @@
1
+ require 'minitest/autorun'
2
+ require 'logger'
3
+
4
+ require_relative '../../lib/loggerator'
5
+
6
+ class Loggerator::TestRequestStore < Minitest::Test
7
+ def setup
8
+ # flush request store
9
+ Thread.current[:request_store] = {}
10
+
11
+ @env = {
12
+ 'REQUEST_ID' => 'abc',
13
+ 'REQUEST_IDS' => %w[ abc def ]
14
+ }
15
+ end
16
+
17
+ def test_seeds_request_id
18
+ Loggerator::RequestStore.seed(@env)
19
+
20
+ assert_equal 'abc,def', Loggerator::RequestStore.store[:request_id]
21
+ end
22
+
23
+ def test_seeds_request_context
24
+ Loggerator::RequestStore.seed(@env)
25
+
26
+ assert_equal 'abc,def', Loggerator::RequestStore.store[:request_context][:request_id]
27
+ end
28
+
29
+ def test_is_cleared_by_clear!
30
+ Loggerator::RequestStore.seed(@env)
31
+ Loggerator::RequestStore.clear!
32
+
33
+ assert_nil Loggerator::RequestStore.store[:request_id]
34
+ end
35
+ end
@@ -0,0 +1,73 @@
1
+ require 'minitest/autorun'
2
+ require 'logger'
3
+
4
+ require_relative '../lib/loggerator'
5
+
6
+ class TestLoggerator < Minitest::Test
7
+ include Loggerator
8
+
9
+ def setup
10
+ # flush request store
11
+ Thread.current[:request_store] = {}
12
+
13
+ self.default_context = {}
14
+ end
15
+
16
+ def test_logs_in_structured_format
17
+ out, err = capture_subprocess_io do
18
+ log(foo: "bar", baz: 42)
19
+ end
20
+
21
+ assert_equal out, "foo=bar baz=42\n"
22
+ assert_equal err, ''
23
+ end
24
+
25
+ def test_re_raises_errors
26
+ assert_raises(RuntimeError) do
27
+ capture_subprocess_io do
28
+ log(foo: 'bar') { raise RuntimeError }
29
+ end
30
+ end
31
+ end
32
+
33
+ def test_supports_blocks_to_log_stages_and_elapsed
34
+ out, _ = capture_subprocess_io do
35
+ log(foo: 'bar') { }
36
+ end
37
+
38
+ assert_equal out, "foo=bar at=start\n" \
39
+ + "foo=bar at=finish elapsed=0.000\n"
40
+ end
41
+
42
+ def test_merges_default_context_with_eq
43
+ # testing both methods
44
+ self.default_context = { app: 'my_app' }
45
+
46
+ out, _ = capture_subprocess_io do
47
+ log(foo: 'bar')
48
+ end
49
+
50
+ assert_equal out, "app=my_app foo=bar\n"
51
+ end
52
+
53
+ def test_suports_a_log_context
54
+ out, _ = capture_subprocess_io do
55
+ self.log_context(app: 'my_app') do
56
+ log(foo: 'bar')
57
+ end
58
+ end
59
+
60
+ assert_equal out, "app=my_app foo=bar\n"
61
+ end
62
+
63
+ def test_log_context_merged_with_default_context
64
+ out, _ = capture_subprocess_io do
65
+ self.default_context = { app: 'my_app' }
66
+ self.log_context(foo: 'bar') do
67
+ log(bah: 'boo')
68
+ end
69
+ end
70
+
71
+ assert_equal out, "app=my_app foo=bar bah=boo\n"
72
+ end
73
+ end
metadata ADDED
@@ -0,0 +1,67 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: loggerator
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Joshua Mervine
8
+ - Reid MacDonald
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2016-08-23 00:00:00.000000000 Z
13
+ dependencies: []
14
+ description: Simple web application extension for logging, following the 12factor
15
+ pattern.
16
+ email:
17
+ - joshua@mervine.net
18
+ - reidmix@gmail.com
19
+ executables: []
20
+ extensions: []
21
+ extra_rdoc_files: []
22
+ files:
23
+ - lib/generators/loggerator/log_generator.rb
24
+ - lib/generators/templates/log.rb.erb
25
+ - lib/loggerator.rb
26
+ - lib/loggerator/loggerator.rb
27
+ - lib/loggerator/metrics.rb
28
+ - lib/loggerator/middleware.rb
29
+ - lib/loggerator/middleware/request_id.rb
30
+ - lib/loggerator/middleware/request_store.rb
31
+ - lib/loggerator/namespace.rb
32
+ - lib/loggerator/rails.rb
33
+ - lib/loggerator/request_store.rb
34
+ - lib/loggerator/test.rb
35
+ - test/loggerator/middleware/request_id_test.rb
36
+ - test/loggerator/middleware/request_store_test.rb
37
+ - test/loggerator/request_store_test.rb
38
+ - test/loggerator_test.rb
39
+ homepage: https://github.com/heroku/loggerator
40
+ licenses:
41
+ - MIT
42
+ metadata: {}
43
+ post_install_message:
44
+ rdoc_options: []
45
+ require_paths:
46
+ - lib
47
+ required_ruby_version: !ruby/object:Gem::Requirement
48
+ requirements:
49
+ - - ">="
50
+ - !ruby/object:Gem::Version
51
+ version: '0'
52
+ required_rubygems_version: !ruby/object:Gem::Requirement
53
+ requirements:
54
+ - - ">="
55
+ - !ruby/object:Gem::Version
56
+ version: '0'
57
+ requirements: []
58
+ rubyforge_project:
59
+ rubygems_version: 2.5.1
60
+ signing_key:
61
+ specification_version: 4
62
+ summary: 'loggerator: A Log Helper'
63
+ test_files:
64
+ - test/loggerator/middleware/request_id_test.rb
65
+ - test/loggerator/middleware/request_store_test.rb
66
+ - test/loggerator/request_store_test.rb
67
+ - test/loggerator_test.rb