loggerator 0.0.1

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.
@@ -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