timber 1.0.3 → 1.0.4
Sign up to get free protection for your applications and to get access to all the features.
- 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
|