timberio 1.0.0.beta1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (133) hide show
  1. checksums.yaml +7 -0
  2. data/.codeclimate.yml +34 -0
  3. data/.gitignore +14 -0
  4. data/Appraisals +37 -0
  5. data/Gemfile +22 -0
  6. data/LICENSE +38 -0
  7. data/README.md +22 -0
  8. data/Rakefile +4 -0
  9. data/TODO +4 -0
  10. data/benchmark/README.md +26 -0
  11. data/benchmark/rails_request.rb +68 -0
  12. data/benchmark/support/rails.rb +69 -0
  13. data/circle.yml +27 -0
  14. data/docs/installation/rails_on_heroku.md +31 -0
  15. data/docs/installation/rails_over_http.md +22 -0
  16. data/gemfiles/rails_3.0.X.gemfile +25 -0
  17. data/gemfiles/rails_3.1.X.gemfile +25 -0
  18. data/gemfiles/rails_3.2.X.gemfile +25 -0
  19. data/gemfiles/rails_4.0.X.gemfile +26 -0
  20. data/gemfiles/rails_4.1.X.gemfile +26 -0
  21. data/gemfiles/rails_4.2.X.gemfile +26 -0
  22. data/gemfiles/rails_5.0.X.gemfile +26 -0
  23. data/gemfiles/rails_edge.gemfile +27 -0
  24. data/lib/timber/api_settings.rb +17 -0
  25. data/lib/timber/bootstrap.rb +45 -0
  26. data/lib/timber/config.rb +25 -0
  27. data/lib/timber/context.rb +76 -0
  28. data/lib/timber/context_snapshot.rb +64 -0
  29. data/lib/timber/contexts/dynamic_values.rb +59 -0
  30. data/lib/timber/contexts/exception.rb +40 -0
  31. data/lib/timber/contexts/http_request.rb +22 -0
  32. data/lib/timber/contexts/http_requests/action_controller_specific.rb +48 -0
  33. data/lib/timber/contexts/http_requests/rack/params.rb +26 -0
  34. data/lib/timber/contexts/http_requests/rack.rb +105 -0
  35. data/lib/timber/contexts/http_response.rb +19 -0
  36. data/lib/timber/contexts/http_responses/action_controller.rb +76 -0
  37. data/lib/timber/contexts/logger.rb +33 -0
  38. data/lib/timber/contexts/organization.rb +33 -0
  39. data/lib/timber/contexts/organizations/action_controller.rb +34 -0
  40. data/lib/timber/contexts/server.rb +21 -0
  41. data/lib/timber/contexts/servers/heroku_specific.rb +48 -0
  42. data/lib/timber/contexts/sql_queries/active_record.rb +30 -0
  43. data/lib/timber/contexts/sql_queries/active_record_specific/binds.rb +37 -0
  44. data/lib/timber/contexts/sql_queries/active_record_specific.rb +59 -0
  45. data/lib/timber/contexts/sql_query.rb +18 -0
  46. data/lib/timber/contexts/template_render.rb +17 -0
  47. data/lib/timber/contexts/template_renders/action_view.rb +29 -0
  48. data/lib/timber/contexts/template_renders/action_view_specific.rb +51 -0
  49. data/lib/timber/contexts/user.rb +39 -0
  50. data/lib/timber/contexts/users/action_controller.rb +34 -0
  51. data/lib/timber/contexts.rb +23 -0
  52. data/lib/timber/current_context.rb +58 -0
  53. data/lib/timber/current_line_indexes.rb +35 -0
  54. data/lib/timber/frameworks/rails.rb +24 -0
  55. data/lib/timber/frameworks.rb +21 -0
  56. data/lib/timber/internal_logger.rb +35 -0
  57. data/lib/timber/log_device.rb +40 -0
  58. data/lib/timber/log_devices/heroku_logplex/hybrid_formatter.rb +14 -0
  59. data/lib/timber/log_devices/heroku_logplex.rb +14 -0
  60. data/lib/timber/log_devices/http/log_pile.rb +86 -0
  61. data/lib/timber/log_devices/http/log_truck/delivery.rb +116 -0
  62. data/lib/timber/log_devices/http/log_truck.rb +87 -0
  63. data/lib/timber/log_devices/http.rb +28 -0
  64. data/lib/timber/log_devices/io/formatter.rb +46 -0
  65. data/lib/timber/log_devices/io/hybrid_formatter.rb +41 -0
  66. data/lib/timber/log_devices/io/hybrid_hidden_formatter.rb +36 -0
  67. data/lib/timber/log_devices/io/json_formatter.rb +11 -0
  68. data/lib/timber/log_devices/io/logfmt_formatter.rb +11 -0
  69. data/lib/timber/log_devices/io.rb +41 -0
  70. data/lib/timber/log_devices.rb +4 -0
  71. data/lib/timber/log_line.rb +33 -0
  72. data/lib/timber/logger.rb +20 -0
  73. data/lib/timber/macros/compactor.rb +16 -0
  74. data/lib/timber/macros/date_formatter.rb +9 -0
  75. data/lib/timber/macros/deep_merger.rb +11 -0
  76. data/lib/timber/macros/logfmt_encoder.rb +77 -0
  77. data/lib/timber/macros.rb +4 -0
  78. data/lib/timber/patterns/delegated_singleton.rb +21 -0
  79. data/lib/timber/patterns/to_json.rb +22 -0
  80. data/lib/timber/patterns/to_logfmt.rb +9 -0
  81. data/lib/timber/patterns.rb +3 -0
  82. data/lib/timber/probe.rb +21 -0
  83. data/lib/timber/probes/action_controller_base.rb +31 -0
  84. data/lib/timber/probes/action_dispatch_debug_exceptions.rb +57 -0
  85. data/lib/timber/probes/active_support_log_subscriber/action_controller.rb +15 -0
  86. data/lib/timber/probes/active_support_log_subscriber/action_view.rb +26 -0
  87. data/lib/timber/probes/active_support_log_subscriber/active_record.rb +13 -0
  88. data/lib/timber/probes/active_support_log_subscriber.rb +62 -0
  89. data/lib/timber/probes/heroku.rb +30 -0
  90. data/lib/timber/probes/logger.rb +31 -0
  91. data/lib/timber/probes/rack.rb +36 -0
  92. data/lib/timber/probes/server.rb +18 -0
  93. data/lib/timber/probes.rb +24 -0
  94. data/lib/timber/version.rb +3 -0
  95. data/lib/timber.rb +27 -0
  96. data/spec/spec_helper.rb +27 -0
  97. data/spec/support/action_controller.rb +4 -0
  98. data/spec/support/action_view.rb +4 -0
  99. data/spec/support/active_record.rb +28 -0
  100. data/spec/support/coveralls.rb +2 -0
  101. data/spec/support/rails/templates/_partial.html +1 -0
  102. data/spec/support/rails/templates/template.html +1 -0
  103. data/spec/support/rails.rb +33 -0
  104. data/spec/support/simplecov.rb +9 -0
  105. data/spec/support/socket_hostname.rb +12 -0
  106. data/spec/support/timber.rb +23 -0
  107. data/spec/support/timecop.rb +3 -0
  108. data/spec/support/webmock.rb +2 -0
  109. data/spec/timber/bootstrap_spec.rb +31 -0
  110. data/spec/timber/context_snapshot_spec.rb +10 -0
  111. data/spec/timber/context_spec.rb +4 -0
  112. data/spec/timber/contexts/exception_spec.rb +34 -0
  113. data/spec/timber/contexts/organizations/action_controller_spec.rb +49 -0
  114. data/spec/timber/contexts/users/action_controller_spec.rb +65 -0
  115. data/spec/timber/current_line_indexes_spec.rb +40 -0
  116. data/spec/timber/frameworks/rails_spec.rb +9 -0
  117. data/spec/timber/log_devices/heroku_logplex_spec.rb +45 -0
  118. data/spec/timber/log_devices/http/log_truck/delivery_spec.rb +66 -0
  119. data/spec/timber/log_devices/http/log_truck_spec.rb +65 -0
  120. data/spec/timber/log_devices/io/hybrid_hidden_formatter_spec.rb +28 -0
  121. data/spec/timber/log_line_spec.rb +49 -0
  122. data/spec/timber/macros/compactor_spec.rb +19 -0
  123. data/spec/timber/macros/logfmt_encoder_spec.rb +89 -0
  124. data/spec/timber/patterns/to_json_spec.rb +40 -0
  125. data/spec/timber/probes/action_controller_base_spec.rb +43 -0
  126. data/spec/timber/probes/action_controller_log_subscriber/action_controller_spec.rb +35 -0
  127. data/spec/timber/probes/action_controller_log_subscriber/action_view_spec.rb +44 -0
  128. data/spec/timber/probes/action_controller_log_subscriber/active_record_spec.rb +26 -0
  129. data/spec/timber/probes/action_dispatch_debug_exceptions_spec.rb +45 -0
  130. data/spec/timber/probes/logger_spec.rb +20 -0
  131. data/spec/timber/probes/rack_spec.rb +26 -0
  132. data/timberio.gemspec +20 -0
  133. metadata +210 -0
@@ -0,0 +1,14 @@
1
+ require File.join(File.dirname(__FILE__), "heroku_logplex", "hybrid_formatter")
2
+
3
+ module Timber
4
+ module LogDevices
5
+ class HerokuLogplex < IO
6
+ def initialize(_options = {})
7
+ super(STDOUT)
8
+ if formatter.is_a?(IO::HybridFormatter)
9
+ formatter.extend HybridFormatter
10
+ end
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,86 @@
1
+ require "thread"
2
+
3
+ module Timber
4
+ module LogDevices
5
+ class HTTP < LogDevice
6
+ # This is a thread safe queue for transporting logs to the Timber API.
7
+ # TODO: Have these log lines persist to a file where
8
+ # a daemon can pick them up.
9
+ class LogPile
10
+ class << self
11
+ def each(&block)
12
+ instances.values.each(&block)
13
+ end
14
+
15
+ def get(application_key)
16
+ instances[application_key] ||= new(application_key)
17
+ end
18
+
19
+ private
20
+ def instances
21
+ @instances ||= {}
22
+ end
23
+ end
24
+
25
+ attr_reader :application_key
26
+
27
+ def initialize(application_key)
28
+ @application_key = application_key
29
+ @mutex = Mutex.new
30
+ end
31
+
32
+ def drop(log_line)
33
+ mutex.synchronize do
34
+ log_lines << log_line
35
+ end
36
+ rescue LogLine::InvalidMessageError => e
37
+ # Ignore the error and log it.
38
+ Config.logger.error(e)
39
+ rescue Exception => e
40
+ # Fail safe to ensure the Timber gem never fails the app.
41
+ Config.logger.exception(e)
42
+ end
43
+
44
+ def empty(&_block)
45
+ if log_lines.any?
46
+ copy = log_lines_copy
47
+ yield(copy) if block_given?
48
+ remove(copy)
49
+ self
50
+ end
51
+ end
52
+
53
+ def size
54
+ log_lines.size
55
+ end
56
+
57
+ private
58
+ def mutex
59
+ @mutex
60
+ end
61
+
62
+ def remove(log_lines_copy)
63
+ mutex.synchronize do
64
+ # Delete items by object_id since we are working
65
+ # with the same object. Do not use equality here.
66
+ log_lines_copy.each do |l1|
67
+ log_lines.delete_if { |l2| l2.object_id == l1.object_id }
68
+ end
69
+ end
70
+ end
71
+
72
+ def log_lines_copy
73
+ mutex.synchronize do
74
+ # Copy the array structure so we aren't dealing with
75
+ # a changing array, but do not copy the items.
76
+ log_lines.clone
77
+ end
78
+ end
79
+
80
+ def log_lines
81
+ @log_lines ||= []
82
+ end
83
+ end
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,116 @@
1
+ require "base64"
2
+ require "net/http"
3
+ require "net/https"
4
+ require "uri"
5
+
6
+ module Timber
7
+ module LogDevices
8
+ class HTTP < LogDevice
9
+ class LogTruck
10
+ class Delivery
11
+ class DeliveryError < StandardError; end
12
+
13
+ API_URI = URI.parse("https://timber-odin.herokuapp.com/agent_log_frames")
14
+ CONTENT_TYPE = 'application/json'.freeze
15
+ READ_TIMEOUT_SECONDS = 35.freeze
16
+ RETRY_BACKOFF_SECONDS = 1.freeze
17
+ RETRY_COUNT = 4.freeze
18
+ USER_AGENT = "Timber Ruby Gem/#{Timber::VERSION}".freeze
19
+
20
+ HTTPS = Net::HTTP.new(API_URI.host, API_URI.port).tap do |https|
21
+ https.use_ssl = true
22
+ https.read_timeout = READ_TIMEOUT_SECONDS
23
+ end
24
+
25
+ attr_reader :application_key, :log_lines
26
+
27
+ def initialize(application_key, log_lines)
28
+ @application_key = application_key
29
+ @log_lines = log_lines
30
+ end
31
+
32
+ def deliver!(retry_count = 0)
33
+ Config.logger.debug("Attempting delivery of: #{body_json}")
34
+ request!
35
+ # Catch them all because of all the unknown exceptions that can happen during
36
+ # a http request.
37
+ rescue Exception => e
38
+ # Ensure that we are always returning a consistent error.
39
+ # This ensures we handle it appropriately and don't kill the
40
+ # thread above.
41
+ Config.logger.warn("Failed delivery: #{e.message}")
42
+
43
+ retry_count += 1
44
+ if retry_count <= RETRY_COUNT
45
+ backoff_seconds = RETRY_BACKOFF_SECONDS ** retry_count
46
+ Config.logger.warn("Backing off #{backoff_seconds} seconds")
47
+ sleep backoff_seconds
48
+ Config.logger.warn("Retrying, attempt #{retry_count}")
49
+ deliver!(retry_count)
50
+ else
51
+ Config.logger.warn("Retry attempts exceeded, dropping logs")
52
+ raise DeliveryError.new(e.message)
53
+ end
54
+ end
55
+
56
+ private
57
+ def https
58
+ @https ||= HTTPS
59
+ end
60
+
61
+ def request!
62
+ https.request(new_request).tap do |res|
63
+ code = res.code.to_i
64
+ if code < 200 || code >= 300
65
+ raise DeliveryError.new("Bad response from Timber API - #{res.code}: #{res.body}")
66
+ end
67
+ Config.logger.debug("Success! #{code}: #{res.body}")
68
+ end
69
+ end
70
+
71
+ def new_request
72
+ Net::HTTP::Post.new(API_URI.request_uri).tap do |req|
73
+ req['Authorization'] = authorization_payload
74
+ req['Body-Checksum'] = body_checksum # the API checks for duplicate requests
75
+ req['Content-Type'] = CONTENT_TYPE
76
+ req['Log-Line-Count'] = log_lines.size # additional check to ensure the correct # of log lines were sent
77
+ req['User-Agent'] = USER_AGENT
78
+ req.body = body_json
79
+ end
80
+ end
81
+
82
+ # Used by the API to check for duplicate requests.
83
+ def body_checksum
84
+ @body_checksum ||= Digest::MD5.hexdigest(body_json)
85
+ end
86
+
87
+ def body_json
88
+ return @body_json if defined?(@body_json)
89
+ # Build the json as a string since it is more efficient.
90
+ # We are also working with string upstream for the same reason.
91
+ @body_json ||= <<-JSON
92
+ {"agent_log_frame": {"log_lines": #{log_lines_json}}}
93
+ JSON
94
+ @body_json.strip!
95
+ @body_json
96
+ end
97
+
98
+ def log_lines_json
99
+ return @log_lines_json if defined?(@log_lines_json)
100
+ @log_lines_json = "["
101
+ last_index = log_lines.size - 1
102
+ log_lines.each_with_index do |log_line, index|
103
+ @log_lines_json += log_line.to_json
104
+ @log_lines_json += ", " if index != last_index
105
+ end
106
+ @log_lines_json += "]"
107
+ end
108
+
109
+ def authorization_payload
110
+ @authorization_payload ||= "Basic #{Base64.strict_encode64(application_key).chomp}"
111
+ end
112
+ end
113
+ end
114
+ end
115
+ end
116
+ end
@@ -0,0 +1,87 @@
1
+ require "uri"
2
+ require "net/http"
3
+ require "net/https"
4
+ require File.join(File.dirname(__FILE__), "log_truck", "delivery")
5
+
6
+ module Timber
7
+ module LogDevices
8
+ class HTTP < LogDevice
9
+ # Temporary class for alpha / beta purposes.
10
+ # Log lines will be written to a file where a daemon
11
+ # will pick them up. Most of this code will be moved
12
+ # to that daemon.
13
+ class LogTruck
14
+ THROTTLE_SECONDS = 3.freeze
15
+
16
+ class NoPayloadError < ArgumentError; end
17
+
18
+ class << self
19
+ def start!(options = {}, &_block)
20
+ return if @thread && @thread.alive?
21
+
22
+ # Old school options to support ruby 1.9 :(
23
+ options[:throttle_seconds] = THROTTLE_SECONDS if !options.key?(:throttle_seconds)
24
+ Config.logger.debug("Starting log truck with a #{options[:throttle_seconds]} second throttle")
25
+
26
+ # A new thread for looping and monitoring. We need to
27
+ # use a thread so that we can share memory.
28
+ @thread = Thread.new do
29
+ # ensure we always deliver upon exiting
30
+ at_exit { deliver }
31
+
32
+ # Keep looking for logs
33
+ loop do
34
+ deliver
35
+
36
+ # Yield a block, primarily for testing purposes
37
+ yield(Thread.current) if block_given?
38
+
39
+ # Throttle to reduce checking the pile
40
+ sleep options[:throttle_seconds]
41
+ end
42
+ end
43
+
44
+ rescue Exception => e
45
+ # failsafe to ensure we don't kill the app
46
+ Config.logger.exception(e)
47
+ end
48
+
49
+ # Deliver, return LogTruck object, otherwise
50
+ # raise an error.
51
+ def deliver
52
+ log_truck = nil
53
+ LogPile.each do |log_pile|
54
+ log_pile.empty do |log_lines|
55
+ # LogPile only empties if no exception is raised
56
+ begin
57
+ # This will retry a number of times. If we can't get it during the retries
58
+ # we drop the logs. Note, this strategy will improve when we write to a file
59
+ # and use an actual agent.
60
+ log_truck = new(log_pile.application_key, log_lines).tap(&:deliver!)
61
+ rescue Delivery::DeliveryError => e
62
+ Config.logger.exception(e)
63
+ # TODO: How do we handle server timeouts? The request could have still been processed.
64
+ end
65
+ end
66
+ end
67
+ log_truck
68
+ end
69
+ end
70
+
71
+ attr_reader :application_key, :log_lines
72
+
73
+ def initialize(application_key, log_lines)
74
+ if log_lines.empty?
75
+ raise NoPayloadError.new("a truck must contain a payload (at least one log line)")
76
+ end
77
+ @application_key = application_key
78
+ @log_lines = log_lines
79
+ end
80
+
81
+ def deliver!
82
+ Delivery.new(application_key, log_lines).deliver!
83
+ end
84
+ end
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,28 @@
1
+ require File.join(File.dirname(__FILE__), "http", "log_pile")
2
+ require File.join(File.dirname(__FILE__), "http", "log_truck")
3
+
4
+ module Timber
5
+ module LogDevices
6
+ class HTTP < LogDevice
7
+ SPLIT_LINES = false
8
+
9
+ attr_reader :application_key
10
+
11
+ def initialize(application_key = nil)
12
+ @application_key = application_key || Config.application_key
13
+ if @application_key.nil?
14
+ raise ArgumentError.new("A Timber application_key is required")
15
+ end
16
+ LogTruck.start!
17
+ end
18
+
19
+ def close(*args)
20
+ end
21
+
22
+ private
23
+ def write_log_line(log_line)
24
+ LogPile.get(application_key).drop(log_line)
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,46 @@
1
+ module Timber
2
+ module LogDevices
3
+ class IO < LogDevice
4
+ class Formatter
5
+ # Do not change this, the API matches on it. Otherwise nothing
6
+ # get parsed.
7
+ CALLOUT = "@timber.io "
8
+ CALLOUT_END = "@original "
9
+
10
+ # Embed in a String to clear all previous ANSI sequences.
11
+ CLEAR = "\e[0m"
12
+ BOLD = "\e[1m"
13
+
14
+ # Colors
15
+ BLACK = "\e[30m"
16
+ DARK_GRAY = "\e[1;30m"
17
+ RED = "\e[31m"
18
+ GREEN = "\e[32m"
19
+ YELLOW = "\e[33m"
20
+ BLUE = "\e[34m"
21
+ MAGENTA = "\e[35m"
22
+ CYAN = "\e[36m"
23
+ WHITE = "\e[37m"
24
+
25
+ def initialize(options = {})
26
+ @ansi_format = options.key?(:ansi_format) ? options[:ansi_format] == true : true
27
+ end
28
+
29
+ def ansi_format?
30
+ @ansi_format == true
31
+ end
32
+
33
+ def format(_log_line)
34
+ raise NotImplementedError.new("#format is not implemented")
35
+ end
36
+
37
+ private
38
+ def ansi_format(*args)
39
+ text = args.pop
40
+ return text unless ansi_format?
41
+ "#{args.join}#{text}#{CLEAR}"
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,41 @@
1
+ module Timber
2
+ module LogDevices
3
+ class IO < LogDevice
4
+ class HybridFormatter < Formatter
5
+ def initialize(options = {})
6
+ super
7
+ @date_prefix = options.key?(:date_prefix) ? options[:date_prefix] : false
8
+ end
9
+
10
+ def date_prefix?
11
+ @date_prefix == true
12
+ end
13
+
14
+ def format(log_line)
15
+ "#{log_line.message}#{context_message(log_line)}"
16
+ end
17
+
18
+ private
19
+ def base_message(log_line)
20
+ text = ""
21
+ if date_prefix?
22
+ text << "#{log_line.formatted_dt} "
23
+ end
24
+ text << log_line.message
25
+ text
26
+ end
27
+
28
+ def context_message(log_line)
29
+ # The callout must be before the formatting, otherwise we leave
30
+ # the message ending with a color formatting and not a reset.
31
+ # Anything before the callout modifies the original message.
32
+ CALLOUT + ansi_format(DARK_GRAY, encoded_context(log_line))
33
+ end
34
+
35
+ def encoded_context(log_line)
36
+ log_line.context_snapshot.to_logfmt
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,36 @@
1
+ module Timber
2
+ module LogDevices
3
+ class IO < LogDevice
4
+ class HybridHiddenFormatter < HybridFormatter
5
+ CLEAR_SEQUENCE = "\e8\e[K".freeze
6
+ CLEAR_STEP_SIZE = 20.freeze
7
+ SAVE_CURSOR_POSITION = "\e7".freeze
8
+
9
+ def format(log_line)
10
+ "#{SAVE_CURSOR_POSITION}#{context_message(log_line)}#{base_message(log_line)}"
11
+ end
12
+
13
+ private
14
+ def context_message(log_line)
15
+ text = encoded_context(log_line)
16
+ position = CLEAR_STEP_SIZE
17
+ sequence_size = CLEAR_SEQUENCE.size
18
+ step_size = sequence_size + CLEAR_STEP_SIZE
19
+ while position < text.length
20
+ # ensure we don't insert before a \
21
+ while text[position - 1] == "\\"
22
+ position += 1
23
+ end
24
+ text.insert(position, CLEAR_SEQUENCE)
25
+ position += step_size
26
+ end
27
+ ansi_format(DARK_GRAY, "#{CALLOUT}#{CLEAR_SEQUENCE}#{text} #{CALLOUT_END}#{CLEAR_SEQUENCE}")
28
+ end
29
+
30
+ def encoded_context(log_line)
31
+ log_line.context_snapshot.to_logfmt
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,11 @@
1
+ module Timber
2
+ module LogDevices
3
+ class IO < LogDevice
4
+ class JSONFormatter < Formatter
5
+ def format(log_line)
6
+ CALLOUT + log_line.to_json
7
+ end
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,11 @@
1
+ module Timber
2
+ module LogDevices
3
+ class IO < LogDevice
4
+ class LogfmtFormatter < Formatter
5
+ def format(log_line)
6
+ @CALLOUT + log_line.to_logfmt
7
+ end
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,41 @@
1
+ require File.join(File.dirname(__FILE__), "io", "formatter")
2
+ require File.join(File.dirname(__FILE__), "io", "hybrid_formatter")
3
+ require File.join(File.dirname(__FILE__), "io", "hybrid_hidden_formatter")
4
+ require File.join(File.dirname(__FILE__), "io", "json_formatter")
5
+ require File.join(File.dirname(__FILE__), "io", "logfmt_formatter")
6
+
7
+ module Timber
8
+ module LogDevices
9
+ # The purpose of a Timber log device is to take the raw log message and enrich it
10
+ # with the current context.
11
+ #
12
+ # The IO log device works with any IO object. That is, any object that
13
+ # response to #write(message).
14
+ class IO < LogDevice
15
+ attr_reader :formatter
16
+
17
+ # Instantiates a new Timber IO log device.
18
+ #
19
+ # @param io [IO] any object the responds to #write(message)
20
+ def initialize(io = STDOUT, options = {})
21
+ io.sync = true if io.respond_to?(:sync=) # ensures logs are written immediately instead of being buffered by ruby
22
+ @formatter = options[:formatter] || HybridHiddenFormatter.new
23
+ @io = io
24
+ end
25
+
26
+ def close(*_args)
27
+ io.close
28
+ end
29
+
30
+ private
31
+ def write_log_line(log_line)
32
+ formatted_message = formatter.format(log_line)
33
+ io.write(formatted_message + "\n")
34
+ end
35
+
36
+ def io
37
+ @io
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,4 @@
1
+ # Order is relevant
2
+ require File.join(File.dirname(__FILE__), "log_devices", "io")
3
+ require File.join(File.dirname(__FILE__), "log_devices", "heroku_logplex")
4
+ require File.join(File.dirname(__FILE__), "log_devices", "http")
@@ -0,0 +1,33 @@
1
+ module Timber
2
+ class LogLine
3
+ include Patterns::ToJSON
4
+ include Patterns::ToLogfmt
5
+
6
+ # Raised when there is an issue with the message being passed.
7
+ # Note: this is handled in Logger
8
+ class InvalidMessageError < ArgumentError; end
9
+
10
+ attr_reader :context_snapshot, :dt, :line_indexes, :message
11
+
12
+ def initialize(message)
13
+ @dt = Time.now.utc # Capture the time as soon as possible
14
+ message = message.to_s
15
+ if message.bytesize > APISettings::MESSAGE_BYTE_SIZE_MAX
16
+ Config.logger.warn("Log line message is too long, truncating")
17
+ message = message.byteslice(0, APISettings::MESSAGE_BYTE_SIZE_MAX)
18
+ end
19
+ @message = message
20
+ CurrentLineIndexes.log_line_added(self) # Bump the indexes
21
+ @context_snapshot = CurrentContext.snapshot
22
+ end
23
+
24
+ def formatted_dt
25
+ @formatted_dt ||= Macros::DateFormatter.format(dt)
26
+ end
27
+
28
+ private
29
+ def json_payload
30
+ @json_payload ||= {:dt => formatted_dt, :message => message}.merge(context_snapshot.as_json)
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,20 @@
1
+ require "logger"
2
+
3
+ module Timber
4
+ # A simple interface to instantiate a logger. It does a couple of things:
5
+ # 1. Simplifies Rails logger instantiation across Rails versions. This
6
+ # helps with simplifying the Readme / install instructions.
7
+ # 2. Serves as a placeholder should we want to extend the logger and add
8
+ # Timber specific functionality.
9
+ module Logger
10
+ def self.new(logger_or_logdev = nil)
11
+ logger = if logger_or_logdev.is_a?(::Logger)
12
+ logger_or_logdev
13
+ else
14
+ Frameworks.logger(logger_or_logdev)
15
+ end
16
+ logger.extend(self)
17
+ logger
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,16 @@
1
+ module Timber
2
+ module Macros
3
+ module Compactor
4
+ def self.compact(hash)
5
+ new_hash = {}
6
+ hash.each do |k, v|
7
+ deep_v = v.is_a?(Hash) ? compact(v) : v
8
+ if !deep_v.nil? && deep_v != [] && deep_v != {}
9
+ new_hash[k] = deep_v
10
+ end
11
+ end
12
+ new_hash
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,9 @@
1
+ module Timber
2
+ module Macros
3
+ module DateFormatter
4
+ def self.format(dt)
5
+ dt.send(APISettings::DATE_FORMAT, APISettings::DATE_FORMAT_PRECISION)
6
+ end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,11 @@
1
+ module Timber
2
+ module Macros
3
+ # Deep merges hash keys
4
+ module DeepMerger
5
+ def self.merge(first, second)
6
+ merger = proc { |_key, v1, v2| Hash === v1 && Hash === v2 ? v1.merge(v2, &merger) : v2 }
7
+ first.merge(second, &merger)
8
+ end
9
+ end
10
+ end
11
+ end