timber 1.0.13 → 1.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.
Files changed (46) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +278 -121
  3. data/lib/timber.rb +1 -0
  4. data/lib/timber/context.rb +1 -1
  5. data/lib/timber/contexts.rb +29 -2
  6. data/lib/timber/contexts/runtime.rb +24 -0
  7. data/lib/timber/contexts/system.rb +19 -0
  8. data/lib/timber/current_context.rb +14 -5
  9. data/lib/timber/event.rb +1 -1
  10. data/lib/timber/events.rb +11 -6
  11. data/lib/timber/events/controller_call.rb +1 -1
  12. data/lib/timber/events/custom.rb +1 -1
  13. data/lib/timber/events/exception.rb +1 -1
  14. data/lib/timber/events/{http_request.rb → http_server_request.rb} +1 -1
  15. data/lib/timber/events/{http_response.rb → http_server_response.rb} +2 -1
  16. data/lib/timber/events/sql_query.rb +2 -1
  17. data/lib/timber/events/template_render.rb +2 -1
  18. data/lib/timber/frameworks/rails.rb +12 -1
  19. data/lib/timber/log_devices/http.rb +29 -24
  20. data/lib/timber/log_entry.rb +23 -9
  21. data/lib/timber/logger.rb +20 -6
  22. data/lib/timber/probes.rb +1 -3
  23. data/lib/timber/probes/active_support_tagged_logging.rb +0 -43
  24. data/lib/timber/probes/rails_rack_logger.rb +1 -1
  25. data/lib/timber/rack_middlewares.rb +12 -0
  26. data/lib/timber/rack_middlewares/http_context.rb +30 -0
  27. data/lib/timber/util.rb +1 -0
  28. data/lib/timber/util/struct.rb +16 -0
  29. data/lib/timber/version.rb +1 -1
  30. data/spec/README.md +23 -0
  31. data/spec/support/timber.rb +1 -1
  32. data/spec/timber/contexts_spec.rb +49 -0
  33. data/spec/timber/events_spec.rb +1 -1
  34. data/spec/timber/log_devices/http_spec.rb +7 -7
  35. data/spec/timber/log_entry_spec.rb +15 -0
  36. data/spec/timber/logger_spec.rb +14 -10
  37. data/spec/timber/probes/action_controller_log_subscriber_spec.rb +6 -7
  38. data/spec/timber/probes/action_dispatch_debug_exceptions_spec.rb +1 -1
  39. data/spec/timber/probes/action_view_log_subscriber_spec.rb +2 -2
  40. data/spec/timber/probes/rails_rack_logger_spec.rb +3 -3
  41. data/spec/timber/rack_middlewares/http_context_spec.rb +47 -0
  42. data/timber.gemspec +1 -0
  43. metadata +31 -8
  44. data/lib/timber/contexts/tags.rb +0 -22
  45. data/lib/timber/probes/rack_http_context.rb +0 -51
  46. data/spec/timber/probes/rack_http_context_spec.rb +0 -50
@@ -17,6 +17,7 @@ require "timber/log_devices"
17
17
  require "timber/log_entry"
18
18
  require "timber/logger"
19
19
  require "timber/probes"
20
+ require "timber/rack_middlewares"
20
21
 
21
22
  # Load frameworks
22
23
  require "timber/frameworks"
@@ -17,7 +17,7 @@ module Timber
17
17
  end
18
18
 
19
19
  def to_json(options = {})
20
- Util::Hash.compact(as_json).to_json(options)
20
+ as_json.to_json(options)
21
21
  end
22
22
 
23
23
  def to_msgpack(*args)
@@ -1,11 +1,38 @@
1
1
  require "timber/contexts/custom"
2
2
  require "timber/contexts/http"
3
3
  require "timber/contexts/organization"
4
- require "timber/contexts/tags"
4
+ require "timber/contexts/runtime"
5
+ require "timber/contexts/system"
5
6
  require "timber/contexts/user"
6
7
 
7
8
  module Timber
8
- # @private
9
+ # Namespace for all Timber supported Contexts.
9
10
  module Contexts
11
+ # Protocol for casting objects into a `Timber::Context`.
12
+ #
13
+ # @example Casting a hash
14
+ # Timber::Contexts.build(deploy: {version: "1.0.0"})
15
+ def self.build(obj)
16
+ if obj.is_a?(::Timber::Context)
17
+ obj
18
+ elsif obj.respond_to?(:to_timber_context)
19
+ obj.to_timber_context
20
+ elsif obj.is_a?(Hash) && obj.length == 1
21
+ type = obj.keys.first
22
+ data = obj.values.first
23
+
24
+ Contexts::Custom.new(
25
+ type: type,
26
+ data: data
27
+ )
28
+ elsif obj.is_a?(Struct) && obj.respond_to?(:type)
29
+ Contexts::Custom.new(
30
+ type: obj.type,
31
+ data: obj.respond_to?(:to_h) ? obj.to_h : Timber::Util::Struct.to_hash(obj) # ruby 1.9.3 does not have to_h
32
+ )
33
+ else
34
+ nil
35
+ end
36
+ end
10
37
  end
11
38
  end
@@ -0,0 +1,24 @@
1
+ module Timber
2
+ module Contexts
3
+ # Tracks OS level process information, such as the process ID.
4
+ class Runtime < Context
5
+ @keyspace = :runtime
6
+
7
+ attr_reader :application, :class_name, :file, :function, :line, :module_name
8
+
9
+ def initialize(attributes)
10
+ @application = attributes[:application]
11
+ @class_name = attributes[:class_name]
12
+ @file = attributes[:file]
13
+ @function = attributes[:function]
14
+ @line = attributes[:line]
15
+ @module_name = attributes[:module_name]
16
+ end
17
+
18
+ def as_json(_options = {})
19
+ {application: application, class_name: class_name, file: file, function: function,
20
+ line: line, module_name: module_name}
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,19 @@
1
+ module Timber
2
+ module Contexts
3
+ # Tracks OS level process information, such as the process ID.
4
+ class System < Context
5
+ @keyspace = :system
6
+
7
+ attr_reader :pid
8
+
9
+ def initialize(attributes)
10
+ @pid = attributes[:pid] || raise(ArgumentError.new(":pid is required"))
11
+ @pid = @pid.to_s
12
+ end
13
+
14
+ def as_json(_options = {})
15
+ {pid: pid}
16
+ end
17
+ end
18
+ end
19
+ end
@@ -38,9 +38,17 @@ module Timber
38
38
  # @note Because context is included with every log line, it is recommended that you limit this
39
39
  # to only neccessary data.
40
40
  #
41
- # @example Adding a custom context
42
- # custom_context = Timber::Contexts::Custom.new(type: :organization, data: %{id: 1, name: "Timber"})
43
- # Timber::CurrentContext.with(custom_context) do
41
+ # @example Adding a custom context with a map
42
+ # Timber::CurrentContext.with({build: {version: "1.0.0"}}) do
43
+ # # ... anything logged here will include the context ...
44
+ # end
45
+ #
46
+ # @example Adding a custom context with a struct
47
+ # BuildContext = Struct.new(:version) do
48
+ # def type; :build; end
49
+ # end
50
+ # build_context = BuildContext.new("1.0.0")
51
+ # Timber::CurrentContext.with(build_context) do
44
52
  # # ... anything logged here will include the context ...
45
53
  # end
46
54
  # # Be sure to checkout Timber::Contexts! These are officially supported and many of these
@@ -67,8 +75,9 @@ module Timber
67
75
  #
68
76
  # @note Because context is included with every log line, it is recommended that you limit this
69
77
  # to only neccessary data.
70
- def add(*contexts)
71
- contexts.each do |context|
78
+ def add(*objects)
79
+ objects.each do |object|
80
+ context = Contexts.build(object) # Normalizes objects into a Timber::Context descendant.
72
81
  key = context.keyspace
73
82
  json = context.as_json # Convert to json now so that we aren't doing it for every line
74
83
  if key == :custom
@@ -11,7 +11,7 @@ module Timber
11
11
  end
12
12
 
13
13
  def to_json(options = {})
14
- Util::Hash.compact(as_json).to_json(options)
14
+ as_json.to_json(options)
15
15
  end
16
16
 
17
17
  def to_msgpack(*args)
@@ -1,12 +1,13 @@
1
1
  require "timber/events/controller_call"
2
2
  require "timber/events/custom"
3
3
  require "timber/events/exception"
4
- require "timber/events/http_request"
5
- require "timber/events/http_response"
4
+ require "timber/events/http_server_request"
5
+ require "timber/events/http_server_response"
6
6
  require "timber/events/sql_query"
7
7
  require "timber/events/template_render"
8
8
 
9
9
  module Timber
10
+ # Namespace for all Timber supported events.
10
11
  module Events
11
12
  # Protocol for casting objects into a `Timber::Event`.
12
13
  #
@@ -17,17 +18,21 @@ module Timber
17
18
  obj
18
19
  elsif obj.respond_to?(:to_timber_event)
19
20
  obj.to_timber_event
20
- elsif obj.is_a?(Hash) && obj.key?(:message) && obj.key?(:type) && obj.key?(:data)
21
+ elsif obj.is_a?(Hash) && obj.key?(:message) && obj.length == 2
22
+ event = obj.select { |k,v| k != :message }
23
+ type = event.keys.first
24
+ data = event.values.first
25
+
21
26
  Events::Custom.new(
22
- type: obj[:type],
27
+ type: type,
23
28
  message: obj[:message],
24
- data: obj[:data]
29
+ data: data
25
30
  )
26
31
  elsif obj.is_a?(Struct) && obj.respond_to?(:message) && obj.respond_to?(:type)
27
32
  Events::Custom.new(
28
33
  type: obj.type,
29
34
  message: obj.message,
30
- data: obj.respond_to?(:hash) ? obj.hash : obj.to_h # ruby 1.9.3 does not have to_h
35
+ data: obj.respond_to?(:to_h) ? obj.to_h : Timber::Util::Struct.to_hash(obj) # ruby 1.9.3 does not have to_h :(
31
36
  )
32
37
  else
33
38
  nil
@@ -22,7 +22,7 @@ module Timber
22
22
  alias to_h to_hash
23
23
 
24
24
  def as_json(_options = {})
25
- {:controller_call => to_hash}
25
+ {:server_side_app => {:controller_call => to_hash}}
26
26
  end
27
27
 
28
28
  def message
@@ -31,7 +31,7 @@ module Timber
31
31
  alias to_h to_hash
32
32
 
33
33
  def as_json(_options = {})
34
- {:custom => to_hash}
34
+ {:server_side_app => {:custom => to_hash}}
35
35
  end
36
36
 
37
37
  def to_json(options = {})
@@ -19,7 +19,7 @@ module Timber
19
19
  alias to_h to_hash
20
20
 
21
21
  def as_json(_options = {})
22
- {:exception => to_hash}
22
+ {:server_side_app => {:exception => to_hash}}
23
23
  end
24
24
 
25
25
  def message
@@ -32,7 +32,7 @@ module Timber
32
32
  hash = to_hash
33
33
  hash[:headers] = Util::Hash.compact(hash[:headers])
34
34
  hash = Util::Hash.compact(hash)
35
- {:http_request => hash}
35
+ {:server_side_app => {:http_request => hash}}
36
36
  end
37
37
 
38
38
  def message
@@ -10,6 +10,7 @@ module Timber
10
10
  def initialize(attributes)
11
11
  @status = attributes[:status] || raise(ArgumentError.new(":status is required"))
12
12
  @time_ms = attributes[:time_ms] || raise(ArgumentError.new(":time_ms is required"))
13
+ @time_ms = @time_ms.round(6)
13
14
  @additions = attributes[:additions]
14
15
  end
15
16
 
@@ -19,7 +20,7 @@ module Timber
19
20
  alias to_h to_hash
20
21
 
21
22
  def as_json(_options = {})
22
- {:http_response => to_hash}
23
+ {:server_side_app => {:http_server_response => to_hash}}
23
24
  end
24
25
 
25
26
  def message
@@ -10,6 +10,7 @@ module Timber
10
10
  def initialize(attributes)
11
11
  @sql = attributes[:sql] || raise(ArgumentError.new(":sql is required"))
12
12
  @time_ms = attributes[:time_ms] || raise(ArgumentError.new(":time_ms is required"))
13
+ @time_ms = @time_ms.round(6)
13
14
  @message = attributes[:message] || raise(ArgumentError.new(":message is required"))
14
15
  end
15
16
 
@@ -19,7 +20,7 @@ module Timber
19
20
  alias to_h to_hash
20
21
 
21
22
  def as_json(_options = {})
22
- {:sql_query => to_hash}
23
+ {:server_side_app => {:sql_query => to_hash}}
23
24
  end
24
25
  end
25
26
  end
@@ -11,6 +11,7 @@ module Timber
11
11
  @message = attributes[:message] || raise(ArgumentError.new(":message is required"))
12
12
  @name = attributes[:name] || raise(ArgumentError.new(":name is required"))
13
13
  @time_ms = attributes[:time_ms] || raise(ArgumentError.new(":time_ms is required"))
14
+ @time_ms = @time_ms.round(6)
14
15
  end
15
16
 
16
17
  def to_hash
@@ -19,7 +20,7 @@ module Timber
19
20
  alias to_h to_hash
20
21
 
21
22
  def as_json(_options = {})
22
- {:template_render => to_hash}
23
+ {:server_side_app => {:template_render => to_hash}}
23
24
  end
24
25
  end
25
26
  end
@@ -5,7 +5,18 @@ module Timber
5
5
  class Railtie < ::Rails::Railtie
6
6
  config.timber = Config.instance
7
7
  config.before_initialize do
8
- Probes.insert!(config.app_middleware, ::Rails::Rack::Logger)
8
+ Probes.insert!
9
+ Timber::Frameworks::Rails.insert_middlewares(config.app_middleware)
10
+ end
11
+ end
12
+
13
+ def self.insert_middlewares(middleware)
14
+ var_name = :"@_timber_middlewares_inserted"
15
+ return true if middleware.instance_variable_defined?(var_name) && middleware.instance_variable_get(var_name) == true
16
+ # Rails uses a proxy :/, so we need to do this instance variable hack
17
+ middleware.instance_variable_set(var_name, true)
18
+ Timber::RackMiddlewares.middlewares.each do |m|
19
+ middleware.insert_before ::Rails::Rack::Logger, m
9
20
  end
10
21
  end
11
22
  end
@@ -1,7 +1,7 @@
1
1
  module Timber
2
2
  module LogDevices
3
- # A log device that buffers and delivers log messages over HTTPS to the Timber API.
4
- # It uses batches, keep-alive connections, and messagepack to delivery logs with
3
+ # A highly efficient log device that buffers and delivers log messages over HTTPS to
4
+ # the Timber API. It uses batches, keep-alive connections, and msgpack to deliver logs with
5
5
  # high-throughput and little overhead.
6
6
  #
7
7
  # See {#initialize} for options and more details.
@@ -29,9 +29,7 @@ module Timber
29
29
  end
30
30
 
31
31
  def full?
32
- @lock.synchronize do
33
- size >= @max_size
34
- end
32
+ size >= @max_size
35
33
  end
36
34
 
37
35
  def size
@@ -61,11 +59,9 @@ module Timber
61
59
  end
62
60
 
63
61
  TIMBER_URL = "https://logs.timber.io/frames".freeze
62
+ ACCEPT = "application/json".freeze
64
63
  CONTENT_TYPE = "application/msgpack".freeze
65
- USER_AGENT = "Timber Ruby Gem/#{Timber::VERSION}".freeze
66
- DELIVERY_FREQUENCY_SECONDS = 2.freeze
67
- RETRY_LIMIT = 5.freeze
68
- BACKOFF_RATE_SECONDS = 3.freeze
64
+ USER_AGENT = "Timber Ruby/#{Timber::VERSION} (HTTP)".freeze
69
65
 
70
66
 
71
67
  # Instantiates a new HTTP log device that can be passed to {Timber::Logger#initialize}.
@@ -81,14 +77,17 @@ module Timber
81
77
  # @param api_key [String] The API key provided to you after you add your application to
82
78
  # [Timber](https://timber.io).
83
79
  # @param [Hash] options the options to create a HTTP log device with.
84
- # @option attributes [Symbol] :batch_size (500) Determines the maximum of log lines in each HTTP
85
- # payload. If the queue exceeds this limit a HTTP request will be issued.
80
+ # @option attributes [Symbol] :batch_size (1000) Determines the maximum of log lines in
81
+ # each HTTP payload. If the queue exceeds this limit an HTTP request will be issued. Bigger
82
+ # payloads mean higher throughput, but also use more memory. Timber will not accept
83
+ # payloads larger than 1mb.
86
84
  # @option attributes [Symbol] :debug (false) Whether to print debug output or not. This is also
87
85
  # inferred from ENV['debug']. Output will be sent to `Timber::Config.logger`.
88
- # @option attributes [Symbol] :flush_interval (2) How often the client should
89
- # attempt to deliver logs to the Timber API. The HTTP client buffers logs and this
90
- # options represents how often that will happen, assuming `:batch_byte_size` is not met.
91
- # @option attributes [Symbol] :requests_per_conn (1000) The number of requests to send over a
86
+ # @option attributes [Symbol] :flush_interval (1) How often the client should
87
+ # attempt to deliver logs to the Timber API in fractional seconds. The HTTP client buffers
88
+ # logs and this options represents how often that will happen, assuming `:batch_byte_size`
89
+ # is not met.
90
+ # @option attributes [Symbol] :requests_per_conn (2500) The number of requests to send over a
92
91
  # single persistent connection. After this number is met, the connection will be closed
93
92
  # and a new one will be opened.
94
93
  # @option attributes [Symbol] :request_queue (SizedQueue.new(3)) The request queue object that queues Net::HTTP
@@ -110,9 +109,9 @@ module Timber
110
109
  @api_key = api_key
111
110
  @debug = options[:debug] || ENV['debug']
112
111
  @timber_url = URI.parse(options[:timber_url] || ENV['TIMBER_URL'] || TIMBER_URL)
113
- @batch_size = options[:batch_size] || 500
114
- @flush_interval = options[:flush_interval] || 2 # 2 seconds
115
- @requests_per_conn = options[:requests_per_conn] || 1_000
112
+ @batch_size = options[:batch_size] || 1_000
113
+ @flush_interval = options[:flush_interval] || 1 # 1 second
114
+ @requests_per_conn = options[:requests_per_conn] || 2_500
116
115
  @msg_queue = LogMsgQueue.new(@batch_size)
117
116
  @request_queue = options[:request_queue] || SizedQueue.new(3)
118
117
  @requests_in_flight = 0
@@ -128,6 +127,7 @@ module Timber
128
127
  def write(msg)
129
128
  @msg_queue.enqueue(msg)
130
129
  if @msg_queue.full?
130
+ logger.debug("Flushing timber buffer via write") if debug?
131
131
  flush
132
132
  end
133
133
  true
@@ -146,17 +146,17 @@ module Timber
146
146
  end
147
147
 
148
148
  def flush
149
+ @last_flush = Time.now
149
150
  msgs = @msg_queue.flush
150
151
  return if msgs.empty?
151
152
 
152
153
  req = Net::HTTP::Post.new(@timber_url.path)
153
- req['Accept'] = "application/json"
154
+ req['Accept'] = ACCEPT
154
155
  req['Authorization'] = authorization_payload
155
156
  req['Content-Type'] = CONTENT_TYPE
156
157
  req['User-Agent'] = USER_AGENT
157
158
  req.body = msgs.to_msgpack
158
159
  @request_queue.enq(req)
159
- @last_flush = Time.now
160
160
  end
161
161
 
162
162
  def intervaled_flush
@@ -165,11 +165,12 @@ module Timber
165
165
  loop do
166
166
  begin
167
167
  if intervaled_flush_ready?
168
+ logger.debug("Flushing timber buffer via the interval") if debug?
168
169
  flush
169
170
  end
170
171
  sleep(0.1)
171
172
  rescue Exception => e
172
- logger.error("Timber intervaled flush failed: #{e.inspect}")
173
+ logger.error("Timber intervaled flush failed: #{e.inspect}\n\n#{e.backtrace}")
173
174
  end
174
175
  end
175
176
  end
@@ -192,8 +193,12 @@ module Timber
192
193
  http.start do |conn|
193
194
  num_reqs = 0
194
195
  while num_reqs < @requests_per_conn
196
+ if debug?
197
+ logger.debug("Waiting on next Timber request")
198
+ logger.debug("Number of threads waiting on Timber request queue: #{@request_queue.num_waiting}")
199
+ end
200
+
195
201
  # Blocks waiting for a request.
196
- logger.info("Waiting on next Timber request") if debug?
197
202
  req = @request_queue.deq
198
203
  @requests_in_flight += 1
199
204
  resp = nil
@@ -206,13 +211,13 @@ module Timber
206
211
  @requests_in_flight -= 1
207
212
  end
208
213
  num_reqs += 1
209
- logger.info("Timber request successful: #{resp.code}") if debug?
214
+ logger.debug("Timber request successful: #{resp.code}") if debug?
210
215
  end
211
216
  end
212
217
  rescue => e
213
218
  logger.error("Timber request error: #{e.message}") if debug?
214
219
  ensure
215
- logger.info("Finishing Timber HTTP connection") if debug?
220
+ logger.debug("Finishing Timber HTTP connection") if debug?
216
221
  http.finish if http.started?
217
222
  end
218
223
  end
@@ -3,38 +3,50 @@ module Timber
3
3
  # `Logger` and the log device that you set it up with.
4
4
  class LogEntry #:nodoc:
5
5
  DT_PRECISION = 6.freeze
6
+ SCHEMA = "https://raw.githubusercontent.com/timberio/log-event-json-schema/1.2.3/schema.json".freeze
6
7
 
7
- attr_reader :level, :time, :progname, :message, :context, :event
8
+ attr_reader :context_snapshot, :event, :level, :message, :progname, :tags, :time
8
9
 
9
10
  # Creates a log entry suitable to be sent to the Timber API.
10
11
  # @param severity [Integer] the log level / severity
11
12
  # @param time [Time] the exact time the log message was written
12
13
  # @param progname [String] the progname scope for the log message
13
- # @param message [#to_json] structured data representing the log line event, this can
14
- # be anything that responds to #to_json
14
+ # @param message [String] Human readable log message.
15
+ # @param context_snapshot [Hash] structured data representing a snapshot of the context at
16
+ # the given point in time.
17
+ # @param event [Timber.Event] structured data representing the log line event. This should be
18
+ # an instance of `Timber.Event`.
15
19
  # @return [LogEntry] the resulting LogEntry object
16
- def initialize(level, time, progname, message, context, event)
20
+ def initialize(level, time, progname, message, context_snapshot, event, tags)
17
21
  @level = level
18
22
  @time = time.utc
19
23
  @progname = progname
20
24
  @message = message
21
- @context = context
25
+ @tags = tags
26
+
27
+ context_snapshot = {} if context_snapshot.nil?
28
+ system_context = Contexts::System.new(pid: Process.pid)
29
+ context_snapshot[system_context.keyspace] = system_context.as_json
30
+
31
+ @context_snapshot = context_snapshot
22
32
  @event = event
23
33
  end
24
34
 
25
35
  def as_json(options = {})
26
36
  options ||= {}
27
- hash = {level: level, dt: formatted_dt, message: message}
37
+ hash = {:level => level, :dt => formatted_dt, :message => message, :tags => tags}
28
38
 
29
39
  if !event.nil?
30
40
  hash[:event] = event
31
41
  end
32
42
 
33
- if !context.nil? && context.length > 0
34
- hash[:context] = context
43
+ if !context_snapshot.nil? && context_snapshot.length > 0
44
+ hash[:context] = context_snapshot
35
45
  end
36
46
 
37
- if options[:only]
47
+ hash[:"$schema"] = SCHEMA
48
+
49
+ hash = if options[:only]
38
50
  hash.select do |key, _value|
39
51
  options[:only].include?(key)
40
52
  end
@@ -45,6 +57,8 @@ module Timber
45
57
  else
46
58
  hash
47
59
  end
60
+
61
+ Util::Hash.compact(hash)
48
62
  end
49
63
 
50
64
  def to_json(options = {})