timber 1.0.3 → 1.0.4
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 +4 -4
- data/README.md +48 -53
- data/lib/timber.rb +1 -0
- data/lib/timber/context.rb +7 -1
- data/lib/timber/contexts.rb +1 -0
- data/lib/timber/contexts/custom.rb +2 -4
- data/lib/timber/contexts/http.rb +2 -4
- data/lib/timber/contexts/organization.rb +2 -4
- data/lib/timber/contexts/tags.rb +22 -0
- data/lib/timber/contexts/user.rb +2 -4
- data/lib/timber/current_context.rb +78 -17
- data/lib/timber/log_devices/http.rb +77 -39
- data/lib/timber/log_devices/http/triggered_buffer.rb +79 -0
- data/lib/timber/logger.rb +39 -10
- data/lib/timber/probes.rb +2 -0
- data/lib/timber/probes/active_support_tagged_logging.rb +106 -0
- data/lib/timber/probes/rack_http_context.rb +1 -1
- data/lib/timber/version.rb +1 -1
- data/spec/timber/log_devices/http/triggered_buffer_spec.rb +58 -0
- data/spec/timber/log_devices/http_spec.rb +85 -62
- data/spec/timber/logger_spec.rb +19 -7
- data/spec/timber/probes/rack_http_context_spec.rb +1 -5
- data/timber.gemspec +2 -2
- metadata +9 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 08cb61746c20295dafbfcf1a098664dd9a7ac0ce
|
4
|
+
data.tar.gz: f7a59dd084e99faac34169d37bc49f33a0aafba0
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 40ffc77bceab9aec868f3f0592054970bd535abcdc6fda63101859605cd68a392faf2a7f7f13970cd6091370f2b711dc2f52072cede1f2d3aee760ef76274ba6
|
7
|
+
data.tar.gz: c2b7781da71ee771da0bb771295b83166f22446cf77bc27905d1a08311079b4b1a7ac3f43ca0011002148dff372f185c3180845905e8b8646960624278122759
|
data/README.md
CHANGED
@@ -11,34 +11,48 @@
|
|
11
11
|
|
12
12
|
|
13
13
|
1. [What is timber?](#what-is-timber)
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
14
|
+
2. [Why timber?](#why-timber)
|
15
|
+
3. [How does it work?](#how-does-it-work)
|
16
|
+
4. [Logging Custom Events](#logging-custom-events)
|
17
|
+
5. [The Timber Console / Pricing](#the-timber-console-pricing)
|
18
|
+
6. [Install](#install)
|
18
19
|
|
19
20
|
|
20
21
|
## What is Timber?
|
21
22
|
|
22
|
-
|
23
|
-
|
24
|
-
|
23
|
+
Glad you asked! :) Timber takes a different approach to logging, in that it automatically
|
24
|
+
enriches and structures your logs without altering the essence of your original log messages.
|
25
|
+
Giving you the best of both worlds: human readable logs *and* rich structured data.
|
25
26
|
|
26
|
-
|
27
|
-
|
27
|
+
And it does so with absolutely no lock-in or risk of code debt. It's just good ol' loggin'™!
|
28
|
+
For example:
|
28
29
|
|
29
|
-
1.
|
30
|
-
2.
|
31
|
-
|
30
|
+
1. The resulting log format, by deafult, is a simple, non-proprietary, JSON structure.
|
31
|
+
2. The [`Timber::Logger`](lib/timber/events) class extends `Logger`, and will never change or
|
32
|
+
extend the public API.
|
33
|
+
3. Where you send your logs is entirely up to you, but we hope you'll check out
|
34
|
+
[timber.io](https://timber.io). We've built a beautiful, modern, and *fast* console specifically
|
35
|
+
for the strutured data captured here.
|
32
36
|
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
+
|
38
|
+
## Why Timber?
|
39
|
+
|
40
|
+
Timber’s philosophy is that application insight should be open and owned by you. It should not
|
41
|
+
require a myriad of services to accomplish. And there is no better vehicle than logging:
|
42
|
+
|
43
|
+
1. The log is immutable and complete. [It is the truth](http://files.timber.io/images/log-is-the-truth.png) :)
|
44
|
+
2. It’s a shared practice that has been around since the dawn of computers.
|
45
|
+
3. It’s baked into every language, library, and framework. Even your own apps!
|
46
|
+
4. The data is open, accessible, and entirely owned by you. Yay!
|
47
|
+
|
48
|
+
The problem is that logs are unstructured, noisy, and hard to use. `grep` can only take you so
|
49
|
+
far! Timber solves this by properly structuring your logs, making them easy to search and
|
50
|
+
visualize -- enabling you to sanely realize the power of your logs.
|
37
51
|
|
38
52
|
|
39
53
|
## How does it work?
|
40
54
|
|
41
|
-
|
55
|
+
Timber automatically structures your logs by taking advantage of public APIs.
|
42
56
|
|
43
57
|
For example, by subscribing to `ActiveSupport::Notifications`, Timber can automatically turn this:
|
44
58
|
|
@@ -76,12 +90,12 @@ Into this:
|
|
76
90
|
```
|
77
91
|
|
78
92
|
It does the same for `http requests`, `sql queries`, `exceptions`, `template renderings`,
|
79
|
-
and any other event your framework logs. (for a full list see `Timber::Events`)
|
93
|
+
and any other event your framework logs. (for a full list see [`Timber::Events`](lib/timber/events))
|
80
94
|
|
81
95
|
|
82
96
|
## Logging Custom Events
|
83
97
|
|
84
|
-
> Another service? More
|
98
|
+
> Another service? More lock-in? :*(
|
85
99
|
|
86
100
|
Nope! Logging custom events is Just Logging™. Check it out:
|
87
101
|
|
@@ -101,41 +115,9 @@ end
|
|
101
115
|
Logger.warn PaymentRejectedEvent.new("abcd1234", 100, "Card expired")
|
102
116
|
```
|
103
117
|
|
104
|
-
(for more examples, see the `Timber::Logger` docs)
|
105
|
-
|
106
|
-
No mention of Timber anywhere! In fact, this approach pushes things the opposite way. What if,
|
107
|
-
as a result of structured logging, you could start decoupling other services from your application?
|
108
|
-
|
109
|
-
Before:
|
110
|
-
|
111
|
-
```
|
112
|
-
|---[HTTP]---> sentry / bugsnag / etc
|
113
|
-
My Application |---[HTTP]---> librato / graphite / etc
|
114
|
-
|---[HTTP]---> new relic / etc
|
115
|
-
|--[STDOUT]--> logs
|
116
|
-
|---> Logging service
|
117
|
-
|---> S3
|
118
|
-
|---> RedShift
|
119
|
-
```
|
120
|
-
|
121
|
-
|
122
|
-
After:
|
123
|
-
|
124
|
-
```
|
125
|
-
|-- sentry / bugsnag / etc
|
126
|
-
|-- librato / graphite / etc
|
127
|
-
My Application |--[STDOUT]--> logs ---> Timber ---> |-- new relic / etc
|
128
|
-
^ |-- S3
|
129
|
-
| |-- RedShift
|
130
|
-
| ^
|
131
|
-
fast, efficient, durable, |
|
132
|
-
replayable, auditable, change any of these without
|
133
|
-
just logging touching your code
|
134
|
-
*and* backfill them!
|
135
|
-
```
|
136
|
-
|
137
|
-
[Mind-blown!](http://i.giphy.com/EldfH1VJdbrwY.gif)
|
118
|
+
(for more examples, see [the `Timber::Logger` docs](lib/timber/logger.rb))
|
138
119
|
|
120
|
+
No mention of Timber anywhere!
|
139
121
|
|
140
122
|
|
141
123
|
## The Timber Console / Pricing
|
@@ -186,6 +168,19 @@ after you add your application.
|
|
186
168
|
|
187
169
|
*Other transport methods coming soon!*
|
188
170
|
|
171
|
+
|
172
|
+
#### Rails TaggedLogging?
|
173
|
+
|
174
|
+
No probs! Use it as normal, Timber will even pull out the tags and include them in the `context`.
|
175
|
+
|
176
|
+
```ruby
|
177
|
+
config.logger = ActiveSupport::TaggedLogging.new(Timber::Logger.new(STDOUT))
|
178
|
+
```
|
179
|
+
|
180
|
+
**Warning**: Tags lack meaningful descriptions, they are a poor mans context. Not to worry though!
|
181
|
+
Timber provides a simple system for adding custom context that you can optionally use. Checkout
|
182
|
+
[the `Timber::CurrentContext` docs](lib/timber/current_context.rb) for examples.
|
183
|
+
|
189
184
|
---
|
190
185
|
|
191
186
|
That's it! Log to your heart's content.
|
data/lib/timber.rb
CHANGED
data/lib/timber/context.rb
CHANGED
@@ -2,8 +2,14 @@ module Timber
|
|
2
2
|
# Base class for all `Timber::Contexts::*` classes.
|
3
3
|
# @private
|
4
4
|
class Context
|
5
|
+
class << self
|
6
|
+
def keyspace
|
7
|
+
@keyspace || raise(NotImplementedError.new)
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
5
11
|
def keyspace
|
6
|
-
|
12
|
+
self.class.keyspace
|
7
13
|
end
|
8
14
|
|
9
15
|
def as_json(options = {})
|
data/lib/timber/contexts.rb
CHANGED
@@ -8,6 +8,8 @@ module Timber
|
|
8
8
|
# # ... anything logged here will have the context ...
|
9
9
|
# end
|
10
10
|
class Custom < Context
|
11
|
+
@keyspace = :custom
|
12
|
+
|
11
13
|
attr_reader :type, :data
|
12
14
|
|
13
15
|
def initialize(attributes)
|
@@ -15,10 +17,6 @@ module Timber
|
|
15
17
|
@data = attributes[:data] || raise(ArgumentError.new(":data is required"))
|
16
18
|
end
|
17
19
|
|
18
|
-
def keyspace
|
19
|
-
:custom
|
20
|
-
end
|
21
|
-
|
22
20
|
def as_json(_options = {})
|
23
21
|
{type => data}
|
24
22
|
end
|
data/lib/timber/contexts/http.rb
CHANGED
@@ -7,6 +7,8 @@ module Timber
|
|
7
7
|
# @note This context should be installed automatically through probes,
|
8
8
|
# such as the {Probes::RackHTTPContext} probe.
|
9
9
|
class HTTP < Context
|
10
|
+
@keyspace = :http
|
11
|
+
|
10
12
|
attr_reader :method, :path, :remote_addr, :request_id
|
11
13
|
|
12
14
|
def initialize(attributes)
|
@@ -16,10 +18,6 @@ module Timber
|
|
16
18
|
@request_id = attributes[:request_id]
|
17
19
|
end
|
18
20
|
|
19
|
-
def keyspace
|
20
|
-
:http
|
21
|
-
end
|
22
|
-
|
23
21
|
def as_json(_options = {})
|
24
22
|
{:method => method, :path => path, :remote_addr => remote_addr, :request_id => request_id}
|
25
23
|
end
|
@@ -16,6 +16,8 @@ module Timber
|
|
16
16
|
# end
|
17
17
|
#
|
18
18
|
class Organization < Context
|
19
|
+
@keyspace = :organization
|
20
|
+
|
19
21
|
attr_reader :id, :name
|
20
22
|
|
21
23
|
def initialize(attributes)
|
@@ -23,10 +25,6 @@ module Timber
|
|
23
25
|
@name = attributes[:name]
|
24
26
|
end
|
25
27
|
|
26
|
-
def keyspace
|
27
|
-
:organization
|
28
|
-
end
|
29
|
-
|
30
28
|
def as_json(_options = {})
|
31
29
|
{id: id, name: name}
|
32
30
|
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
module Timber
|
2
|
+
module Contexts
|
3
|
+
# Adds unnamed tags to the context.
|
4
|
+
#
|
5
|
+
# **Warning:** It is highly recommend that you use custom contexts instead. As they are
|
6
|
+
# more descriptive. This module exists primarily to support the ActiveSupport::TaggedLogging
|
7
|
+
# antipattern.
|
8
|
+
class Tags < Context
|
9
|
+
@keyspace = :tags
|
10
|
+
|
11
|
+
attr_reader :values
|
12
|
+
|
13
|
+
def initialize(attributes)
|
14
|
+
@values = attributes[:values] || raise(ArgumentError.new(":values is required"))
|
15
|
+
end
|
16
|
+
|
17
|
+
def as_json(_options = {})
|
18
|
+
values
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
data/lib/timber/contexts/user.rb
CHANGED
@@ -17,6 +17,8 @@ module Timber
|
|
17
17
|
# end
|
18
18
|
#
|
19
19
|
class User < Context
|
20
|
+
@keyspace = :user
|
21
|
+
|
20
22
|
attr_reader :id, :name
|
21
23
|
|
22
24
|
def initialize(attributes)
|
@@ -24,10 +26,6 @@ module Timber
|
|
24
26
|
@name = attributes[:name]
|
25
27
|
end
|
26
28
|
|
27
|
-
def keyspace
|
28
|
-
:user
|
29
|
-
end
|
30
|
-
|
31
29
|
def as_json(_options = {})
|
32
30
|
{id: id, name: name}
|
33
31
|
end
|
@@ -4,40 +4,101 @@ module Timber
|
|
4
4
|
# Holds the current context in a thread safe memory storage. This context is
|
5
5
|
# appended to every log line. Think of context as join data between your log lines,
|
6
6
|
# allowing you to relate them and filter them appropriately.
|
7
|
+
#
|
8
|
+
# @note Because context is appended to every log line, it is recommended that you limit this
|
9
|
+
# to only neccessary data needed to relate your log lines.
|
7
10
|
class CurrentContext
|
8
11
|
include Singleton
|
9
12
|
|
10
13
|
THREAD_NAMESPACE = :_timber_current_context.freeze
|
11
14
|
|
12
15
|
class << self
|
13
|
-
# Convenience method for {#with}.
|
14
|
-
#
|
15
|
-
# @example Adding a context
|
16
|
-
# custom_context = Timber::Contexts::Custom.new(type: :keyspace, data: %{my: "data"})
|
17
|
-
# Timber::CurrentContext.with(custom_context) do
|
18
|
-
# # ... anything logged here will have the context ...
|
19
|
-
# end
|
16
|
+
# Convenience method for {#with}. See {#with} for full details and examples.
|
20
17
|
def with(*args, &block)
|
21
18
|
instance.with(*args, &block)
|
22
19
|
end
|
20
|
+
|
21
|
+
# Convenience method for {#add}. See {#add} for full details and examples.
|
22
|
+
def add(*args)
|
23
|
+
instance.add(*args)
|
24
|
+
end
|
25
|
+
|
26
|
+
# Convenience method for {#remove}. See {#remove} for full details and examples.
|
27
|
+
def remove(*args)
|
28
|
+
instance.remove(*args)
|
29
|
+
end
|
30
|
+
|
31
|
+
def hash(*args)
|
32
|
+
instance.hash(*args)
|
33
|
+
end
|
23
34
|
end
|
24
35
|
|
25
|
-
# Adds a context
|
26
|
-
|
27
|
-
|
28
|
-
|
36
|
+
# Adds a context and then removes it when the block is finished executing.
|
37
|
+
#
|
38
|
+
# @note Because context is included with every log line, it is recommended that you limit this
|
39
|
+
# to only neccessary data.
|
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
|
44
|
+
# # ... anything logged here will include the context ...
|
45
|
+
# end
|
46
|
+
# # Be sure to checkout Timber::Contexts! These are officially supported and many of these
|
47
|
+
# # will be automatically included via Timber::Probes
|
48
|
+
#
|
49
|
+
# @example Adding multiple contexts
|
50
|
+
# Timber::CurrentContext.with(context1, context2) { ... }
|
51
|
+
def with(*contexts)
|
52
|
+
add(*contexts)
|
29
53
|
yield
|
30
54
|
ensure
|
31
|
-
|
55
|
+
contexts.each do |context|
|
56
|
+
if context.keyspace == :custom
|
57
|
+
# Custom contexts are merged and should be removed the same
|
58
|
+
hash[context.keyspace].delete(context.type)
|
59
|
+
else
|
60
|
+
remove(context)
|
61
|
+
end
|
62
|
+
end
|
32
63
|
end
|
33
64
|
|
34
|
-
|
35
|
-
|
65
|
+
# Adds contexts but does not remove them. See {#with} for automatic maintenance and {#remove}
|
66
|
+
# to remove them yourself.
|
67
|
+
#
|
68
|
+
# @note Because context is included with every log line, it is recommended that you limit this
|
69
|
+
# to only neccessary data.
|
70
|
+
def add(*contexts)
|
71
|
+
contexts.each do |context|
|
72
|
+
key = context.keyspace
|
73
|
+
json = context.as_json # Convert to json now so that we aren't doing it for every line
|
74
|
+
if key == :custom
|
75
|
+
# Custom contexts are merged into the space
|
76
|
+
hash[key] ||= {}
|
77
|
+
hash[key].merge(json)
|
78
|
+
else
|
79
|
+
hash[key] = json
|
80
|
+
end
|
81
|
+
end
|
36
82
|
end
|
37
83
|
|
38
|
-
|
39
|
-
|
40
|
-
|
84
|
+
# Removes a context. This must be a {Timber::Context} type. See {Timber::Contexts} for a list.
|
85
|
+
# If you wish to remove by key, or some other way, use {#hash} and modify the hash accordingly.
|
86
|
+
def remove(*contexts)
|
87
|
+
contexts.each do |context|
|
88
|
+
hash.delete(context.keyspace)
|
41
89
|
end
|
90
|
+
end
|
91
|
+
|
92
|
+
# The internal hash that is maintained. It is recommended that you use {#with} and {#add}
|
93
|
+
# for hash maintenance.
|
94
|
+
def hash
|
95
|
+
Thread.current[THREAD_NAMESPACE] ||= {}
|
96
|
+
end
|
97
|
+
|
98
|
+
# Snapshots the current context so that you get a moment in time representation of the context,
|
99
|
+
# since the context can change as execution proceeds.
|
100
|
+
def snapshot
|
101
|
+
hash.clone
|
102
|
+
end
|
42
103
|
end
|
43
104
|
end
|
@@ -1,19 +1,17 @@
|
|
1
|
-
require "
|
2
|
-
require "msgpack"
|
1
|
+
require "timber/log_devices/http/triggered_buffer"
|
3
2
|
|
4
3
|
module Timber
|
5
4
|
module LogDevices
|
6
|
-
# A log device that buffers and
|
7
|
-
#
|
8
|
-
#
|
5
|
+
# A log device that buffers and delivers log messages over HTTPS to the Timber API in batches.
|
6
|
+
# The buffer and delivery strategy are very efficient and the log messages will be delivered in
|
7
|
+
# msgpack format.
|
8
|
+
#
|
9
|
+
# See {#initialize} for options and more details.
|
9
10
|
class HTTP
|
10
|
-
class DeliveryError < StandardError; end
|
11
|
-
|
12
11
|
API_URI = URI.parse("https://api.timber.io/http_frames")
|
13
|
-
CONTENT_TYPE = "application/
|
12
|
+
CONTENT_TYPE = "application/x-timber-msgpack-frame-1".freeze
|
14
13
|
CONNECTION_HEADER = "keep-alive".freeze
|
15
14
|
USER_AGENT = "Timber Ruby Gem/#{Timber::VERSION}".freeze
|
16
|
-
|
17
15
|
HTTPS = Net::HTTP.new(API_URI.host, API_URI.port).tap do |https|
|
18
16
|
https.use_ssl = true
|
19
17
|
https.read_timeout = 30
|
@@ -23,60 +21,100 @@ module Timber
|
|
23
21
|
end
|
24
22
|
https.open_timeout = 10
|
25
23
|
end
|
24
|
+
DELIVERY_FREQUENCY_SECONDS = 2.freeze
|
25
|
+
RETRY_LIMIT = 3.freeze
|
26
|
+
BACKOFF_RATE_SECONDS = 3.freeze
|
26
27
|
|
27
|
-
DEFAULT_DELIVERY_FREQUENCY = 2.freeze
|
28
28
|
|
29
|
-
# Instantiates a new HTTP log device.
|
29
|
+
# Instantiates a new HTTP log device that can be passed to {Timber::Logger#initialize}.
|
30
30
|
#
|
31
31
|
# @param api_key [String] The API key provided to you after you add your application to
|
32
32
|
# [Timber](https://timber.io).
|
33
33
|
# @param [Hash] options the options to create a HTTP log device with.
|
34
|
-
# @option attributes [Symbol] :
|
34
|
+
# @option attributes [Symbol] :payload_limit_bytes Determines the maximum size in bytes that
|
35
|
+
# and HTTP payload can be. Please see {TriggereBuffer#initialize} for the default.
|
36
|
+
# @option attributes [Symbol] :buffer_limit_bytes Determines the maximum size of the total
|
37
|
+
# buffer. This should be many times larger than the `:payload_limit_bytes`.
|
38
|
+
# Please see {TriggereBuffer#initialize} for the default.
|
39
|
+
# @option attributes [Symbol] :buffer_overflow_handler (nil) When a single message exceeds
|
40
|
+
# `:payload_limit_bytes` or the entire buffer exceeds `:buffer_limit_bytes`, the Proc
|
41
|
+
# passed to this option will be called with the msg that would overflow the buffer. See
|
42
|
+
# the examples on how to use this properly.
|
43
|
+
# @option attributes [Symbol] :delivery_frequency_seconds (2) How often the client should
|
35
44
|
# attempt to deliver logs to the Timber API. The HTTP client buffers logs between calls.
|
45
|
+
#
|
46
|
+
# @example Basic usage
|
47
|
+
# Timber::Logger.new(Timber::LogDevices::HTTP.new("my_timber_api_key"))
|
48
|
+
#
|
49
|
+
# @example Handling buffer overflows
|
50
|
+
# # Persist overflowed lines to a file
|
51
|
+
# # Note: You could write these to any permanent storage.
|
52
|
+
# overflow_log_path = "/path/to/my/overflow_log.log"
|
53
|
+
# overflow_handler = Proc.new { |log_line_msg| File.write(overflow_log_path, log_line_ms) }
|
54
|
+
# http_log_device = Timber::LogDevices::HTTP.new("my_timber_api_key",
|
55
|
+
# buffer_overflow_handler: overflow_handler)
|
56
|
+
# Timber::Logger.new(http_log_device)
|
36
57
|
def initialize(api_key, options = {})
|
37
58
|
@api_key = api_key
|
38
|
-
@buffer =
|
39
|
-
|
40
|
-
|
41
|
-
|
59
|
+
@buffer = TriggeredBuffer.new(
|
60
|
+
payload_limit_bytes: options[:payload_limit_bytes],
|
61
|
+
limit_bytes: options[:buffer_limit_bytes],
|
62
|
+
overflow_handler: options[:buffer_overflow_handler]
|
63
|
+
)
|
64
|
+
@delivery_interval_thread = Thread.new do
|
42
65
|
loop do
|
43
|
-
sleep
|
44
|
-
|
66
|
+
sleep(options[:delivery_frequency_seconds] || DELIVERY_FREQUENCY_SECONDS)
|
67
|
+
buffer_for_delivery = @buffer.reserve
|
68
|
+
if buffer_for_delivery
|
69
|
+
deliver(buffer_for_delivery)
|
70
|
+
end
|
45
71
|
end
|
46
72
|
end
|
47
73
|
end
|
48
74
|
|
75
|
+
# Write a new log line message to the buffer, and deliver if the msg exceeds the
|
76
|
+
# payload limit.
|
49
77
|
def write(msg)
|
50
|
-
@
|
51
|
-
|
52
|
-
|
78
|
+
buffer_for_delivery = @buffer.write(msg)
|
79
|
+
if buffer_for_delivery
|
80
|
+
deliver(buffer_for_delivery)
|
81
|
+
end
|
82
|
+
true
|
53
83
|
end
|
54
84
|
|
85
|
+
# Closes the log device, cleans up, and attempts one last delivery.
|
55
86
|
def close
|
56
|
-
@
|
87
|
+
@delivery_interval_thread.kill
|
88
|
+
buffer_for_delivery = @buffer.reserve
|
89
|
+
if buffer_for_delivery
|
90
|
+
deliver(buffer_for_delivery)
|
91
|
+
end
|
57
92
|
end
|
58
93
|
|
59
94
|
private
|
60
|
-
def deliver
|
61
|
-
|
95
|
+
def deliver(body)
|
96
|
+
Thread.new do
|
97
|
+
RETRY_LIMIT.times do |try_index|
|
98
|
+
request = Net::HTTP::Post.new(API_URI.request_uri).tap do |req|
|
99
|
+
req['Authorization'] = authorization_payload
|
100
|
+
req['Connection'] = CONNECTION_HEADER
|
101
|
+
req['Content-Type'] = CONTENT_TYPE
|
102
|
+
req['User-Agent'] = USER_AGENT
|
103
|
+
req.body = body
|
104
|
+
end
|
62
105
|
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
if code < 200 || code >= 300
|
74
|
-
raise DeliveryError.new("Bad response from Timber API - #{res.code}: #{res.body}")
|
106
|
+
res = HTTPS.request(request)
|
107
|
+
code = res.code.to_i
|
108
|
+
if code < 200 || code >= 300
|
109
|
+
Config.instance.logger.debug("Timber HTTP delivery failed - #{res.code}: #{res.body}")
|
110
|
+
sleep((try_index + 1) * BACKOFF_RATE_SECONDS)
|
111
|
+
else
|
112
|
+
@buffer.remove(body)
|
113
|
+
Config.instance.logger.debug("Timber HTTP delivery successful - #{code}")
|
114
|
+
break # exit the loop
|
115
|
+
end
|
75
116
|
end
|
76
|
-
Config.instance.logger.debug("Success! #{code}: #{res.body}")
|
77
117
|
end
|
78
|
-
|
79
|
-
@buffer.clear
|
80
118
|
end
|
81
119
|
|
82
120
|
def authorization_payload
|
@@ -0,0 +1,79 @@
|
|
1
|
+
require "monitor"
|
2
|
+
|
3
|
+
module Timber
|
4
|
+
module LogDevices
|
5
|
+
class HTTP
|
6
|
+
# Maintains a triggered buffer, where the trigger is {PAYLOAD_LIMIT_BYTES}. Once the buffer
|
7
|
+
# exceeds this limit it will lock and return that buffer up to that point while still making
|
8
|
+
# a new buffer available for writes. This ensures that the HTTP client can attempt to deliver
|
9
|
+
# the buffer contents without blocking execution of the application.
|
10
|
+
#
|
11
|
+
# If the overall buffer exceeeds the overall limit (specified by the `:limit_bytes` option),
|
12
|
+
# then a buffer overflow is triggered. This can be customized using the `:overflow_handler`
|
13
|
+
# option.
|
14
|
+
class TriggeredBuffer
|
15
|
+
DEFAULT_PAYLOAD_LIMIT_BYTES = 5_000_000 # 5mb, the Timber API will not accept messages larger than this
|
16
|
+
DEFAULT_LIMIT_BYTES = 50_000_000 # 50mb
|
17
|
+
|
18
|
+
def initialize(options = {})
|
19
|
+
@buffers = []
|
20
|
+
@monitor = Monitor.new
|
21
|
+
@payload_limit_bytes = options[:payload_limit_bytes] || DEFAULT_PAYLOAD_LIMIT_BYTES
|
22
|
+
@limit_bytes = options[:limit_bytes] || DEFAULT_LIMIT_BYTES
|
23
|
+
@overflow_handler = options[:overflow_handler]
|
24
|
+
end
|
25
|
+
|
26
|
+
def write(msg)
|
27
|
+
if msg.bytesize > @payload_limit_bytes || (msg.bytesize + total_bytesize) > @limit_bytes
|
28
|
+
handle_overflow(msg)
|
29
|
+
return nil
|
30
|
+
end
|
31
|
+
|
32
|
+
@monitor.synchronize do
|
33
|
+
buffer = writable_buffer
|
34
|
+
if @buffers == [] || buffer.nil? || buffer.frozen?
|
35
|
+
@buffers << msg
|
36
|
+
nil
|
37
|
+
elsif (buffer.bytesize + msg.bytesize) > @payload_limit_bytes
|
38
|
+
@buffers << msg
|
39
|
+
buffer.freeze
|
40
|
+
else
|
41
|
+
buffer << msg
|
42
|
+
nil
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def reserve
|
48
|
+
@monitor.synchronize do
|
49
|
+
buffer = writable_buffer
|
50
|
+
if buffer
|
51
|
+
buffer.freeze
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def remove(buffer)
|
57
|
+
@monitor.synchronize do
|
58
|
+
@buffers.delete(buffer)
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
private
|
63
|
+
def total_bytesize
|
64
|
+
@buffers.reduce(0) { |acc, buffer| acc + buffer.bytesize }
|
65
|
+
end
|
66
|
+
|
67
|
+
def writable_buffer
|
68
|
+
@buffers.find { |buffer| !buffer.frozen? }
|
69
|
+
end
|
70
|
+
|
71
|
+
def handle_overflow(msg)
|
72
|
+
if @overflow_handler
|
73
|
+
@overflow_handler.call(msg)
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
data/lib/timber/logger.rb
CHANGED
@@ -81,6 +81,27 @@ module Timber
|
|
81
81
|
end
|
82
82
|
end
|
83
83
|
|
84
|
+
# Structures your log messages into Timber's hybrid format, which makes
|
85
|
+
# it easy to read while also appending the appropriate metadata.
|
86
|
+
#
|
87
|
+
# logger = Timber::Logger.new(STDOUT)
|
88
|
+
# logger.formatter = Timber::JSONFormatter.new
|
89
|
+
#
|
90
|
+
# Example message:
|
91
|
+
#
|
92
|
+
# My log message @timber.io {"level":"info","dt":"2016-09-01T07:00:00.000000-05:00"}
|
93
|
+
#
|
94
|
+
class HybridFormatter < Formatter
|
95
|
+
METADATA_CALLOUT = "@timber.io".freeze
|
96
|
+
|
97
|
+
def call(severity, time, progname, msg)
|
98
|
+
log_entry = build_log_entry(severity, time, progname, msg)
|
99
|
+
metadata = log_entry.to_json(:except => [:message])
|
100
|
+
# use << for concatenation for performance reasons
|
101
|
+
log_entry.message.gsub("\n", "\\n") << " " << METADATA_CALLOUT << " " << metadata << "\n"
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
84
105
|
# Structures your log messages into JSON.
|
85
106
|
#
|
86
107
|
# logger = Timber::Logger.new(STDOUT)
|
@@ -97,24 +118,19 @@ module Timber
|
|
97
118
|
end
|
98
119
|
end
|
99
120
|
|
100
|
-
# Structures your log messages into
|
101
|
-
# it easy to read while also appending the appropriate metadata.
|
121
|
+
# Structures your log messages into JSON.
|
102
122
|
#
|
103
123
|
# logger = Timber::Logger.new(STDOUT)
|
104
124
|
# logger.formatter = Timber::JSONFormatter.new
|
105
125
|
#
|
106
126
|
# Example message:
|
107
127
|
#
|
108
|
-
#
|
128
|
+
# {"level":"info","dt":"2016-09-01T07:00:00.000000-05:00","message":"My log message"}
|
109
129
|
#
|
110
|
-
class
|
111
|
-
METADATA_CALLOUT = "@timber.io".freeze
|
112
|
-
|
130
|
+
class MsgPackFormatter < Formatter
|
113
131
|
def call(severity, time, progname, msg)
|
114
|
-
log_entry = build_log_entry(severity, time, progname, msg)
|
115
|
-
metadata = log_entry.to_json(:except => [:message])
|
116
132
|
# use << for concatenation for performance reasons
|
117
|
-
|
133
|
+
build_log_entry(severity, time, progname, msg).as_json.to_msgpack << "\n"
|
118
134
|
end
|
119
135
|
end
|
120
136
|
|
@@ -127,7 +143,20 @@ module Timber
|
|
127
143
|
# logger.formatter = Timber::Logger::JSONFormatter.new
|
128
144
|
def initialize(*args)
|
129
145
|
super(*args)
|
130
|
-
|
146
|
+
if args.size == 1 and args.first.is_a?(LogDevices::HTTP)
|
147
|
+
self.formatter = MsgPackFormatter.new
|
148
|
+
else
|
149
|
+
self.formatter = HybridFormatter.new
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
def formatter=(value)
|
154
|
+
if @dev.is_a?(Timber::LogDevices::HTTP)
|
155
|
+
raise ArgumentError.new("The formatter cannot be changed when using the " +
|
156
|
+
"Timber::LogDevices::HTTP log device. The MsgPackFormatter must be used for proper " +
|
157
|
+
"delivery.")
|
158
|
+
end
|
159
|
+
super
|
131
160
|
end
|
132
161
|
|
133
162
|
# Backwards compatibility with older ActiveSupport::Logger versions
|
data/lib/timber/probes.rb
CHANGED
@@ -2,6 +2,7 @@ require "timber/probes/action_controller_log_subscriber"
|
|
2
2
|
require "timber/probes/action_dispatch_debug_exceptions"
|
3
3
|
require "timber/probes/action_view_log_subscriber"
|
4
4
|
require "timber/probes/active_record_log_subscriber"
|
5
|
+
require "timber/probes/active_support_tagged_logging"
|
5
6
|
require "timber/probes/rack_http_context"
|
6
7
|
require "timber/probes/rails_rack_logger"
|
7
8
|
|
@@ -14,6 +15,7 @@ module Timber
|
|
14
15
|
ActionDispatchDebugExceptions.insert!
|
15
16
|
ActionViewLogSubscriber.insert!
|
16
17
|
ActiveRecordLogSubscriber.insert!
|
18
|
+
ActiveSupportTaggedLogging.insert!
|
17
19
|
RackHTTPContext.insert!(middleware, insert_before)
|
18
20
|
RailsRackLogger.insert!
|
19
21
|
end
|
@@ -0,0 +1,106 @@
|
|
1
|
+
module Timber
|
2
|
+
module Probes
|
3
|
+
# Reponsible for automatimcally tracking SQL query events in `ActiveRecord`, while still
|
4
|
+
# preserving the default log style.
|
5
|
+
class ActiveSupportTaggedLogging < Probe
|
6
|
+
module FormatterMethods
|
7
|
+
def self.included(mod)
|
8
|
+
mod.module_eval do
|
9
|
+
alias_method :_timber_original_push_tags, :push_tags
|
10
|
+
alias_method :_timber_original_pop_tags, :pop_tags
|
11
|
+
|
12
|
+
def call(severity, timestamp, progname, msg)
|
13
|
+
if is_a?(Timber::Logger::Formatter)
|
14
|
+
# Don't convert the message into a string
|
15
|
+
super(severity, timestamp, progname, msg)
|
16
|
+
else
|
17
|
+
super(severity, timestamp, progname, "#{tags_text}#{msg}")
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def push_tags(*tags)
|
22
|
+
_timber_original_push_tags(*tags).tap do
|
23
|
+
if current_tags.size > 0
|
24
|
+
context = Contexts::Tags.new(values: current_tags)
|
25
|
+
CurrentContext.add(context)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def pop_tags(size = 1)
|
31
|
+
_timber_original_pop_tags(size).tap do
|
32
|
+
if current_tags.size == 0
|
33
|
+
CurrentContext.remove(Contexts::Tags)
|
34
|
+
else
|
35
|
+
context = Contexts::Tags.new(values: current_tags)
|
36
|
+
CurrentContext.add(context)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
module LoggerMethods
|
45
|
+
def self.included(klass)
|
46
|
+
klass.class_eval do
|
47
|
+
alias_method :_timber_original_push_tags, :push_tags
|
48
|
+
alias_method :_timber_original_pop_tags, :pop_tags
|
49
|
+
|
50
|
+
def push_tags(*tags)
|
51
|
+
_timber_original_push_tags(*tags).tap do
|
52
|
+
if current_tags.size > 0
|
53
|
+
context = Contexts::Tags.new(values: current_tags)
|
54
|
+
CurrentContext.add(context)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def pop_tags(size = 1)
|
60
|
+
_timber_original_pop_tags(size).tap do
|
61
|
+
if current_tags.size == 0
|
62
|
+
CurrentContext.remove(Contexts::Tags)
|
63
|
+
else
|
64
|
+
context = Contexts::Tags.new(values: current_tags)
|
65
|
+
CurrentContext.add(context)
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
def add(severity, message = nil, progname = nil, &block)
|
71
|
+
if message.nil?
|
72
|
+
if block_given?
|
73
|
+
message = block.call
|
74
|
+
else
|
75
|
+
message = progname
|
76
|
+
progname = nil #No instance variable for this like Logger
|
77
|
+
end
|
78
|
+
end
|
79
|
+
if @logger.is_a?(Timber::Logger)
|
80
|
+
@logger.add(severity, message, progname)
|
81
|
+
else
|
82
|
+
@logger.add(severity, "#{tags_text}#{message}", progname)
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
def initialize
|
90
|
+
require "active_support/tagged_logging"
|
91
|
+
rescue LoadError => e
|
92
|
+
raise RequirementNotMetError.new(e.message)
|
93
|
+
end
|
94
|
+
|
95
|
+
def insert!
|
96
|
+
if defined?(ActiveSupport::TaggedLogging::Formatter)
|
97
|
+
return true if ActiveSupport::TaggedLogging::Formatter.include?(FormatterMethods)
|
98
|
+
ActiveSupport::TaggedLogging::Formatter.send(:include, FormatterMethods)
|
99
|
+
else
|
100
|
+
return true if ActiveSupport::TaggedLogging.include?(LoggerMethods)
|
101
|
+
ActiveSupport::TaggedLogging.send(:include, LoggerMethods)
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
data/lib/timber/version.rb
CHANGED
@@ -0,0 +1,58 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
describe Timber::LogDevices::HTTP::TriggeredBuffer do
|
4
|
+
describe "#write" do
|
5
|
+
it "should trigger a buffer overflow for large messages" do
|
6
|
+
buffer = described_class.new(:payload_limit_bytes => 10)
|
7
|
+
msg = "a" * 11
|
8
|
+
expect(buffer).to receive(:handle_overflow).exactly(1).times.with(msg)
|
9
|
+
buffer.write(msg)
|
10
|
+
end
|
11
|
+
|
12
|
+
it "should trigger a buffer overflow when exceeding the limit" do
|
13
|
+
buffer = described_class.new(:limit_bytes => 10)
|
14
|
+
msg = "a" * 11
|
15
|
+
expect(buffer).to receive(:handle_overflow).exactly(1).times.with(msg)
|
16
|
+
buffer.write(msg)
|
17
|
+
end
|
18
|
+
|
19
|
+
it "should start a new buffer when empty and append when not" do
|
20
|
+
buffer = described_class.new
|
21
|
+
result = buffer.write("test")
|
22
|
+
expect(result).to be_nil
|
23
|
+
expect(buffer.send(:writable_buffer)).to eq("test")
|
24
|
+
result = buffer.write("again")
|
25
|
+
expect(result).to be_nil
|
26
|
+
expect(buffer.send(:writable_buffer)).to eq("testagain")
|
27
|
+
end
|
28
|
+
|
29
|
+
it "should return the old buffer when it has exceeded it's limit" do
|
30
|
+
buffer = described_class.new(:payload_limit_bytes => 10)
|
31
|
+
msg = "a" * 6
|
32
|
+
result = buffer.write(msg)
|
33
|
+
expect(result).to be_nil
|
34
|
+
result = buffer.write(msg)
|
35
|
+
expect(result).to eq(msg)
|
36
|
+
expect(result).to be_frozen
|
37
|
+
end
|
38
|
+
|
39
|
+
it "should write a new buffer when the latest is frozen" do
|
40
|
+
buffer = described_class.new
|
41
|
+
buffer.write("test")
|
42
|
+
result = buffer.reserve
|
43
|
+
expect(result).to eq("test")
|
44
|
+
buffer.write("again")
|
45
|
+
expect(buffer.send(:writable_buffer)).to eq("again")
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
describe "#reserve" do
|
50
|
+
it "should reserve the latest buffer and freeze it" do
|
51
|
+
buffer = described_class.new
|
52
|
+
buffer.write("test")
|
53
|
+
result = buffer.reserve
|
54
|
+
expect(result).to eq("test")
|
55
|
+
expect(result).to be_frozen
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
@@ -1,62 +1,85 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
4
|
-
#
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
#
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
describe Timber::LogDevices::HTTP do
|
4
|
+
describe "#initialize" do
|
5
|
+
it "should start a thread for delivery" do
|
6
|
+
expect_any_instance_of(described_class).to receive(:deliver).at_least(1).times.and_return(true)
|
7
|
+
http = described_class.new("MYKEY", delivery_frequency_seconds: 0.1)
|
8
|
+
thread = http.instance_variable_get(:@delivery_interval_thread)
|
9
|
+
expect(thread).to be_alive
|
10
|
+
|
11
|
+
http.write("my log message")
|
12
|
+
sleep 0.3 # too fast!
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
describe "#write" do
|
17
|
+
let(:http) { described_class.new("MYKEY") }
|
18
|
+
let(:buffer) { http.instance_variable_get(:@buffer) }
|
19
|
+
|
20
|
+
it "should buffer the messages" do
|
21
|
+
http.write("test log message")
|
22
|
+
expect(buffer.reserve).to eq("test log message")
|
23
|
+
end
|
24
|
+
|
25
|
+
context "with a low payload limit" do
|
26
|
+
let(:http) { described_class.new("MYKEY", :payload_limit_bytes => 20) }
|
27
|
+
|
28
|
+
it "should attempt a delivery when the payload limit is exceeded" do
|
29
|
+
message = "a" * 19
|
30
|
+
http.write(message)
|
31
|
+
expect(http).to receive(:deliver).exactly(1).times.with(message)
|
32
|
+
http.write("my log message")
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
describe "#close" do
|
38
|
+
let(:http) { described_class.new("MYKEY") }
|
39
|
+
|
40
|
+
it "should kill the delivery thread the messages" do
|
41
|
+
http.close
|
42
|
+
thread = http.instance_variable_get(:@delivery_interval_thread)
|
43
|
+
sleep 0.1 # too fast!
|
44
|
+
expect(thread).to_not be_alive
|
45
|
+
end
|
46
|
+
|
47
|
+
it "should attempt a delivery" do
|
48
|
+
message = "a" * 19
|
49
|
+
http.write(message)
|
50
|
+
expect(http).to receive(:deliver).exactly(1).times.with(message)
|
51
|
+
http.close
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
describe "#deliver" do
|
56
|
+
let(:http) { described_class.new("MYKEY") }
|
57
|
+
|
58
|
+
after(:each) { http.close }
|
59
|
+
|
60
|
+
it "should delivery properly and flush the buffer" do
|
61
|
+
stub = stub_request(:post, "https://api.timber.io/http_frames").
|
62
|
+
with(
|
63
|
+
:body => "test log message",
|
64
|
+
:headers => {
|
65
|
+
'Authorization' => 'Basic TVlLRVk=',
|
66
|
+
'Connection' => 'keep-alive',
|
67
|
+
'Content-Type' => 'application/x-timber-msgpack-frame-1',
|
68
|
+
'User-Agent' => "Timber Ruby Gem/#{Timber::VERSION}"
|
69
|
+
}
|
70
|
+
).
|
71
|
+
to_return(:status => 200, :body => "", :headers => {})
|
72
|
+
|
73
|
+
http.write("test log message")
|
74
|
+
buffer = http.instance_variable_get(:@buffer)
|
75
|
+
buffers = buffer.instance_variable_get(:@buffers)
|
76
|
+
expect(buffers.size).to eq(1)
|
77
|
+
body = buffer.reserve
|
78
|
+
thread = http.send(:deliver, body)
|
79
|
+
thread.join
|
80
|
+
|
81
|
+
expect(stub).to have_been_requested.times(1)
|
82
|
+
expect(buffers.size).to eq(0)
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
data/spec/timber/logger_spec.rb
CHANGED
@@ -29,7 +29,7 @@ describe Timber::Logger, :rails_23 => true do
|
|
29
29
|
end
|
30
30
|
|
31
31
|
around(:each) do |example|
|
32
|
-
Timber::CurrentContext.
|
32
|
+
Timber::CurrentContext.with(http_context) do
|
33
33
|
example.run
|
34
34
|
end
|
35
35
|
end
|
@@ -76,13 +76,25 @@ describe Timber::Logger, :rails_23 => true do
|
|
76
76
|
end
|
77
77
|
end
|
78
78
|
|
79
|
-
|
80
|
-
|
79
|
+
if defined?(ActiveSupport::TaggedLogging)
|
80
|
+
context "with TaggedLogging", :rails_23 => false do
|
81
|
+
let(:logger) { ActiveSupport::TaggedLogging.new(Timber::Logger.new(io)) }
|
81
82
|
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
83
|
+
it "should format properly with events" do
|
84
|
+
message = Timber::Events::SQLQuery.new(sql: "select * from users", time_ms: 56, message: "select * from users")
|
85
|
+
logger.tagged("tag") do
|
86
|
+
logger.info(message)
|
87
|
+
end
|
88
|
+
expect(io.string).to eq("select * from users @timber.io {\"level\":\"info\",\"dt\":\"2016-09-01T12:00:00.000000Z\",\"event\":{\"sql_query\":{\"sql\":\"select * from users\",\"time_ms\":56}},\"context\":{\"tags\":[\"tag\"]}}\n")
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
context "with the HTTP log device" do
|
94
|
+
let(:io) { Timber::LogDevices::HTTP.new("my_key") }
|
95
|
+
|
96
|
+
it "should use the msgpack formatter" do
|
97
|
+
expect(logger.formatter).to be_kind_of(Timber::Logger::MsgPackFormatter)
|
86
98
|
end
|
87
99
|
end
|
88
100
|
end
|
@@ -41,11 +41,7 @@ describe Timber::Probes::RackHTTPContext do
|
|
41
41
|
dispatch_rails_request("/rack_http")
|
42
42
|
http_context = Thread.current[:_timber_context][:http]
|
43
43
|
|
44
|
-
expect(http_context).to
|
45
|
-
expect(http_context.method).to eq("GET")
|
46
|
-
expect(http_context.path).to eq("/rack_http")
|
47
|
-
expect(http_context.remote_addr).to eq("123.456.789.10")
|
48
|
-
expect(http_context.request_id).to_not be_nil
|
44
|
+
expect(http_context).to eq({:method=>"GET", :path=>"/rack_http", :remote_addr=>"123.456.789.10", :request_id=>"unique-request-id-1234"})
|
49
45
|
message = "Processing by RackHttpController#index as HTML @timber.io {\"level\":\"info\",\"dt\":\"2016-09-01T12:00:00.000000Z\",\"event\":{\"controller_call\":{\"controller\":\"RackHttpController\",\"action\":\"index\"}},\"context\":{\"http\":{\"method\":\"GET\",\"path\":\"/rack_http\",\"remote_addr\":\"123.456.789.10\",\"request_id\":\"unique-request-id-1234\"}}}\nCompleted 200 OK in 0.0ms (Views: 1.0ms) @timber.io {\"level\":\"info\",\"dt\":\"2016-09-01T12:00:00.000000Z\",\"event\":{\"http_response\":{\"status\":200,\"time_ms\":0.0}},\"context\":{\"http\":{\"method\":\"GET\",\"path\":\"/rack_http\",\"remote_addr\":\"123.456.789.10\",\"request_id\":\"unique-request-id-1234\"}}}\n"
|
50
46
|
expect(io.string).to eq(message)
|
51
47
|
end
|
data/timber.gemspec
CHANGED
@@ -8,8 +8,8 @@ Gem::Specification.new do |s|
|
|
8
8
|
s.platform = Gem::Platform::RUBY
|
9
9
|
s.authors = ["Timber Technologies, Inc."]
|
10
10
|
s.email = ["hi@timber.io"]
|
11
|
-
s.homepage = "
|
12
|
-
s.summary = "
|
11
|
+
s.homepage = "https://github.com/timberio/timber-ruby"
|
12
|
+
s.summary = "Instant log gratification."
|
13
13
|
|
14
14
|
s.required_ruby_version = '>= 1.9.0'
|
15
15
|
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: timber
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.0.
|
4
|
+
version: 1.0.4
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Timber Technologies, Inc.
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2016-12-
|
11
|
+
date: 2016-12-15 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: msgpack
|
@@ -46,6 +46,7 @@ files:
|
|
46
46
|
- lib/timber/contexts/custom.rb
|
47
47
|
- lib/timber/contexts/http.rb
|
48
48
|
- lib/timber/contexts/organization.rb
|
49
|
+
- lib/timber/contexts/tags.rb
|
49
50
|
- lib/timber/contexts/user.rb
|
50
51
|
- lib/timber/current_context.rb
|
51
52
|
- lib/timber/event.rb
|
@@ -61,6 +62,7 @@ files:
|
|
61
62
|
- lib/timber/frameworks/rails.rb
|
62
63
|
- lib/timber/log_devices.rb
|
63
64
|
- lib/timber/log_devices/http.rb
|
65
|
+
- lib/timber/log_devices/http/triggered_buffer.rb
|
64
66
|
- lib/timber/log_entry.rb
|
65
67
|
- lib/timber/logger.rb
|
66
68
|
- lib/timber/probe.rb
|
@@ -72,6 +74,7 @@ files:
|
|
72
74
|
- lib/timber/probes/action_view_log_subscriber/log_subscriber.rb
|
73
75
|
- lib/timber/probes/active_record_log_subscriber.rb
|
74
76
|
- lib/timber/probes/active_record_log_subscriber/log_subscriber.rb
|
77
|
+
- lib/timber/probes/active_support_tagged_logging.rb
|
75
78
|
- lib/timber/probes/rack_http_context.rb
|
76
79
|
- lib/timber/probes/rails_rack_logger.rb
|
77
80
|
- lib/timber/util.rb
|
@@ -92,6 +95,7 @@ files:
|
|
92
95
|
- spec/support/timecop.rb
|
93
96
|
- spec/support/webmock.rb
|
94
97
|
- spec/timber/events_spec.rb
|
98
|
+
- spec/timber/log_devices/http/triggered_buffer_spec.rb
|
95
99
|
- spec/timber/log_devices/http_spec.rb
|
96
100
|
- spec/timber/logger_spec.rb
|
97
101
|
- spec/timber/probes/action_controller_log_subscriber_spec.rb
|
@@ -101,7 +105,7 @@ files:
|
|
101
105
|
- spec/timber/probes/rack_http_context_spec.rb
|
102
106
|
- spec/timber/probes/rails_rack_logger_spec.rb
|
103
107
|
- timber.gemspec
|
104
|
-
homepage:
|
108
|
+
homepage: https://github.com/timberio/timber-ruby
|
105
109
|
licenses: []
|
106
110
|
metadata: {}
|
107
111
|
post_install_message:
|
@@ -123,7 +127,7 @@ rubyforge_project:
|
|
123
127
|
rubygems_version: 2.6.8
|
124
128
|
signing_key:
|
125
129
|
specification_version: 4
|
126
|
-
summary:
|
130
|
+
summary: Instant log gratification.
|
127
131
|
test_files:
|
128
132
|
- spec/spec_helper.rb
|
129
133
|
- spec/support/action_controller.rb
|
@@ -139,6 +143,7 @@ test_files:
|
|
139
143
|
- spec/support/timecop.rb
|
140
144
|
- spec/support/webmock.rb
|
141
145
|
- spec/timber/events_spec.rb
|
146
|
+
- spec/timber/log_devices/http/triggered_buffer_spec.rb
|
142
147
|
- spec/timber/log_devices/http_spec.rb
|
143
148
|
- spec/timber/logger_spec.rb
|
144
149
|
- spec/timber/probes/action_controller_log_subscriber_spec.rb
|