logtail 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (55) hide show
  1. checksums.yaml +7 -0
  2. data/.github/workflows/main.yml +33 -0
  3. data/.gitignore +24 -0
  4. data/.rspec +2 -0
  5. data/CHANGELOG.md +12 -0
  6. data/Gemfile +10 -0
  7. data/LICENSE.md +15 -0
  8. data/README.md +4 -0
  9. data/Rakefile +72 -0
  10. data/lib/logtail.rb +36 -0
  11. data/lib/logtail/config.rb +154 -0
  12. data/lib/logtail/config/integrations.rb +17 -0
  13. data/lib/logtail/context.rb +9 -0
  14. data/lib/logtail/contexts.rb +12 -0
  15. data/lib/logtail/contexts/http.rb +31 -0
  16. data/lib/logtail/contexts/release.rb +52 -0
  17. data/lib/logtail/contexts/runtime.rb +23 -0
  18. data/lib/logtail/contexts/session.rb +24 -0
  19. data/lib/logtail/contexts/system.rb +29 -0
  20. data/lib/logtail/contexts/user.rb +28 -0
  21. data/lib/logtail/current_context.rb +168 -0
  22. data/lib/logtail/event.rb +36 -0
  23. data/lib/logtail/events.rb +10 -0
  24. data/lib/logtail/events/controller_call.rb +44 -0
  25. data/lib/logtail/events/error.rb +40 -0
  26. data/lib/logtail/events/exception.rb +10 -0
  27. data/lib/logtail/events/sql_query.rb +26 -0
  28. data/lib/logtail/events/template_render.rb +25 -0
  29. data/lib/logtail/integration.rb +40 -0
  30. data/lib/logtail/integrator.rb +50 -0
  31. data/lib/logtail/log_devices.rb +8 -0
  32. data/lib/logtail/log_devices/http.rb +368 -0
  33. data/lib/logtail/log_devices/http/flushable_dropping_sized_queue.rb +52 -0
  34. data/lib/logtail/log_devices/http/request_attempt.rb +20 -0
  35. data/lib/logtail/log_entry.rb +110 -0
  36. data/lib/logtail/logger.rb +270 -0
  37. data/lib/logtail/logtail.rb +36 -0
  38. data/lib/logtail/timer.rb +21 -0
  39. data/lib/logtail/util.rb +7 -0
  40. data/lib/logtail/util/non_nil_hash_builder.rb +40 -0
  41. data/lib/logtail/version.rb +3 -0
  42. data/logtail.gemspec +43 -0
  43. data/spec/README.md +13 -0
  44. data/spec/logtail/current_context_spec.rb +113 -0
  45. data/spec/logtail/events/controller_call_spec.rb +12 -0
  46. data/spec/logtail/events/error_spec.rb +15 -0
  47. data/spec/logtail/log_devices/http_spec.rb +185 -0
  48. data/spec/logtail/log_entry_spec.rb +22 -0
  49. data/spec/logtail/logger_spec.rb +227 -0
  50. data/spec/spec_helper.rb +22 -0
  51. data/spec/support/logtail.rb +5 -0
  52. data/spec/support/socket_hostname.rb +12 -0
  53. data/spec/support/timecop.rb +3 -0
  54. data/spec/support/webmock.rb +3 -0
  55. metadata +238 -0
@@ -0,0 +1,52 @@
1
+ require "logtail/config"
2
+ require "logtail/context"
3
+ require "logtail/util"
4
+
5
+ module Logtail
6
+ module Contexts
7
+ # @private
8
+ class Release < Context
9
+ class << self
10
+ # Builds a release context based on environment variables. Simply add the
11
+ # `RELEASE_COMMIT`, `RELEASE_CREATED_AT`, or the `RELEASE_VERSION` env vars
12
+ # to get this context automatially. All are optional, but at least one
13
+ # must be present.
14
+ #
15
+ # If you're on Heroku, simply enable dyno metadata to get this automatically:
16
+ # https://devcenter.heroku.com/articles/dyno-metadata
17
+ def from_env
18
+ commit_hash = ENV['RELEASE_COMMIT'] || ENV['HEROKU_SLUG_COMMIT']
19
+ created_at = ENV['RELEASE_CREATED_AT'] || ENV['HEROKU_RELEASE_CREATED_AT']
20
+ version = ENV['RELEASE_VERSION'] || ENV['HEROKU_RELEASE_VERSION']
21
+
22
+ if commit_hash || created_at || version
23
+ Logtail::Config.instance.debug { "Release env vars detected, adding to context" }
24
+ new(commit_hash: commit_hash, created_at: created_at, version: version)
25
+ else
26
+ Logtail::Config.instance.debug { "Release env vars _not_ detected" }
27
+ nil
28
+ end
29
+ end
30
+ end
31
+
32
+ attr_reader :commit_hash, :created_at, :version
33
+
34
+ def initialize(attributes)
35
+ @commit_hash = attributes[:commit_hash]
36
+ @created_at = attributes[:created_at]
37
+ @version = attributes[:version]
38
+ end
39
+
40
+ # Builds a hash representation containing simple objects, suitable for serialization (JSON).
41
+ def to_hash
42
+ @to_hash ||= {
43
+ release: Util::NonNilHashBuilder.build do |h|
44
+ h.add(:commit_hash, commit_hash)
45
+ h.add(:created_at, created_at)
46
+ h.add(:version, version)
47
+ end
48
+ }
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,23 @@
1
+ require "logtail/context"
2
+
3
+ module Logtail
4
+ module Contexts
5
+ # @private
6
+ class Runtime < Context
7
+ attr_reader :thread_id
8
+
9
+ def initialize(attributes)
10
+ @thread_id = attributes[:thread_id]
11
+ end
12
+
13
+ # Builds a hash representation containing simple objects, suitable for serialization (JSON).
14
+ def to_hash
15
+ @to_hash ||= {
16
+ runtime: Util::NonNilHashBuilder.build do |h|
17
+ h.add(:thread_id, thread_id)
18
+ end
19
+ }
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,24 @@
1
+ require "logtail/context"
2
+ require "logtail/util"
3
+
4
+ module Logtail
5
+ module Contexts
6
+ # @private
7
+ class Session < Context
8
+ attr_reader :id
9
+
10
+ def initialize(attributes)
11
+ @id = attributes[:id]
12
+ end
13
+
14
+ # Builds a hash representation containing simple objects, suitable for serialization (JSON).
15
+ def to_hash
16
+ @to_hash ||= {
17
+ session: Util::NonNilHashBuilder.build do |h|
18
+ h.add(:id, id)
19
+ end
20
+ }
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,29 @@
1
+ require "logtail/context"
2
+ require "logtail/util"
3
+
4
+ module Logtail
5
+ module Contexts
6
+ # The system context tracks OS level process information, such as the process ID.
7
+ #
8
+ # @note This is tracked automatically in {CurrentContext}. When the current context
9
+ # is initialized, the system context gets added automatically.
10
+ class System < Context
11
+ attr_reader :hostname, :pid
12
+
13
+ def initialize(attributes)
14
+ @hostname = attributes[:hostname]
15
+ @pid = attributes[:pid]
16
+ end
17
+
18
+ # Builds a hash representation containing simple objects, suitable for serialization (JSON).
19
+ def to_hash
20
+ @to_hash ||= {
21
+ system: Util::NonNilHashBuilder.build do |h|
22
+ h.add(:hostname, hostname)
23
+ h.add(:pid, pid)
24
+ end
25
+ }
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,28 @@
1
+ require "logtail/context"
2
+ require "logtail/util"
3
+
4
+ module Logtail
5
+ module Contexts
6
+ # @private
7
+ class User < Context
8
+ attr_reader :id, :name, :email
9
+
10
+ def initialize(attributes)
11
+ @id = attributes[:id]
12
+ @name = attributes[:name]
13
+ @email = attributes[:email]
14
+ end
15
+
16
+ # Builds a hash representation containing simple objects, suitable for serialization (JSON).
17
+ def to_hash
18
+ @to_hash ||= {
19
+ user: Util::NonNilHashBuilder.build do |h|
20
+ h.add(:id, id)
21
+ h.add(:name, name)
22
+ h.add(:email, email)
23
+ end
24
+ }
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,168 @@
1
+ require "socket"
2
+
3
+ require "logtail/config"
4
+
5
+ module Logtail
6
+ # Holds the current context in a thread safe memory storage. This context is
7
+ # appended to every log line. Think of context as join data between your log lines,
8
+ # allowing you to relate them and filter them appropriately.
9
+ #
10
+ # @note Because context is appended to every log line, it is recommended that you limit this
11
+ # to only necessary data needed to relate your log lines.
12
+ class CurrentContext
13
+ THREAD_NAMESPACE = :_logtail_current_context.freeze
14
+
15
+ class << self
16
+ # Implements the Singleton pattern in a thread specific way. Each thread receives
17
+ # its own context.
18
+ def instance
19
+ Thread.current[THREAD_NAMESPACE] ||= new
20
+ end
21
+
22
+ # Convenience method for {CurrentContext#add}. See {CurrentContext#add} for more info.
23
+ def add(*args)
24
+ instance.add(*args)
25
+ end
26
+
27
+ # Convenience method for {CurrentContext#fetch}. See {CurrentContext#fetch} for more info.
28
+ def fetch(*args)
29
+ instance.fetch(*args)
30
+ end
31
+
32
+ # Convenience method for {CurrentContext#remove}. See {CurrentContext#remove} for more info.
33
+ def remove(*args)
34
+ instance.remove(*args)
35
+ end
36
+
37
+ # Convenience method for {CurrentContext#reset}. See {CurrentContext#reset} for more info.
38
+ def reset(*args)
39
+ instance.reset(*args)
40
+ end
41
+
42
+ # Convenience method for {CurrentContext#with}. See {CurrentContext#with} for more info.
43
+ def with(*args, &block)
44
+ instance.with(*args, &block)
45
+ end
46
+ end
47
+
48
+ # Adds contexts but does not remove them. See {#with} for automatic maintenance and {#remove}
49
+ # to remove them yourself.
50
+ #
51
+ # @note Because context is included with every log line, it is recommended that you limit this
52
+ # to only necessary data.
53
+ def add(*objects)
54
+ objects.each do |object|
55
+ hash.merge!(object.to_hash)
56
+ end
57
+ expire_cache!
58
+ self
59
+ end
60
+
61
+ # Fetch a specific context by key.
62
+ def fetch(*args)
63
+ hash.fetch(*args)
64
+ end
65
+
66
+ # Removes a context. If you wish to remove by key, or some other way, use {#hash} and
67
+ # modify the hash accordingly.
68
+ def remove(*keys)
69
+ keys.each do |keys|
70
+ hash.delete(keys)
71
+ end
72
+ expire_cache!
73
+ self
74
+ end
75
+
76
+ def replace(hash)
77
+ @hash = hash
78
+ expire_cache!
79
+ self
80
+ end
81
+
82
+ # Resets the context to be blank. Use this carefully! This will remove *any* context,
83
+ # include context that is automatically included with Logtail.
84
+ def reset
85
+ hash.clear
86
+ expire_cache!
87
+ self
88
+ end
89
+
90
+ # Snapshots the current context so that you get a moment in time representation of the context,
91
+ # since the context can change as execution proceeds. Note that individual contexts
92
+ # should be immutable, and we implement snapshot caching as a result of this assumption.
93
+ def snapshot
94
+ @snapshot ||= hash.clone
95
+ end
96
+
97
+ # Adds a context and then removes it when the block is finished executing.
98
+ #
99
+ # @note Because context is included with every log line, it is recommended that you limit this
100
+ # to only necessary data.
101
+ #
102
+ # @example Adding a custom context
103
+ # Logtail::CurrentContext.with({build: {version: "1.0.0"}}) do
104
+ # # ... anything logged here will include the context ...
105
+ # end
106
+ #
107
+ # @note Any custom context needs to have a single root key to be valid. i.e. instead of:
108
+ # Logtail::CurrentContext.with(job_id: "123", job_name: "Refresh User Account")
109
+ #
110
+ # do
111
+ #
112
+ # Logtail::CurrentContext.with(job: {job_id: "123", job_name: "Refresh User Account"})
113
+ #
114
+ # @example Adding multiple contexts
115
+ # Logtail::CurrentContext.with(context1, context2) { ... }
116
+ def with(*objects)
117
+ old_hash = hash.clone
118
+ begin
119
+ add(*objects)
120
+ yield
121
+ ensure
122
+ replace(old_hash)
123
+ end
124
+ end
125
+
126
+ private
127
+ # The internal hash that is maintained. Use {#with} and {#add} for hash maintenance.
128
+ def hash
129
+ @hash ||= build_initial_hash
130
+ end
131
+
132
+ # Builds the initial hash. This is extract into a method to support a threaded
133
+ # environment. Each thread holds it's own context and also needs to instantiate
134
+ # it's hash properly.
135
+ def build_initial_hash
136
+ new_hash = {}
137
+
138
+ # Release context
139
+ release_context = Util::NonNilHashBuilder.build do |h|
140
+ h.add(:commit_hash, ENV['RELEASE_COMMIT'] || ENV['HEROKU_SLUG_COMMIT'])
141
+ h.add(:created_at, ENV['RELEASE_CREATED_AT'] || ENV['HEROKU_RELEASE_CREATED_AT'])
142
+ h.add(:version, ENV['RELEASE_VERSION'] || ENV['HEROKU_RELEASE_VERSION'])
143
+ end
144
+
145
+ if release_context != {}
146
+ new_hash.merge!({release: release_context})
147
+ end
148
+
149
+ # System context
150
+ hostname = Socket.gethostname.force_encoding('UTF-8')
151
+ pid = Process.pid
152
+ system_context = Contexts::System.new(hostname: hostname, pid: pid)
153
+ new_hash.merge!(system_context.to_hash)
154
+
155
+ # Runtime context
156
+ thread_object_id = Thread.current.object_id
157
+ runtime_context = {thread_id: thread_object_id}
158
+ new_hash.merge!({runtime: runtime_context})
159
+
160
+ new_hash
161
+ end
162
+
163
+ # Hook to clear any caching implement in this class
164
+ def expire_cache!
165
+ @snapshot = nil
166
+ end
167
+ end
168
+ end
@@ -0,0 +1,36 @@
1
+ module Logtail
2
+ # Base class for `Logtail::Events::*`
3
+ # @private
4
+ class Event
5
+ attr_reader :message, :metadata
6
+ def initialize(message, metadata)
7
+ @message = message || ""
8
+ @metadata = metadata || {}
9
+ end
10
+
11
+ # This ensures that Logtail events get logged as messages if they are passed to
12
+ # the standard ::Logger.
13
+ #
14
+ # See: https://github.com/ruby/ruby/blob/f6e77b9d3555c1fbaa8aab1cdc0bd6bde95f62c6/lib/logger.rb#L615
15
+ def inspect
16
+ message
17
+ end
18
+
19
+ def to_json(options = {})
20
+ metadata.to_json(options)
21
+ end
22
+
23
+ def to_hash
24
+ metadata
25
+ end
26
+ alias to_h to_hash
27
+
28
+ def to_msgpack(*args)
29
+ metadata.to_msgpack(*args)
30
+ end
31
+
32
+ def to_s
33
+ message
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,10 @@
1
+ require "logtail/events/controller_call"
2
+ require "logtail/events/error"
3
+ require "logtail/events/sql_query"
4
+ require "logtail/events/template_render"
5
+
6
+ module Logtail
7
+ # Namespace for all Logtail supported events.
8
+ module Events
9
+ end
10
+ end
@@ -0,0 +1,44 @@
1
+ require "logtail/util"
2
+ require "logtail/event"
3
+
4
+ module Logtail
5
+ module Events
6
+ # @private
7
+ class ControllerCall < Logtail::Event
8
+ attr_reader :controller, :action, :params, :params_json, :format
9
+
10
+ def initialize(attributes)
11
+ @controller = attributes[:controller]
12
+ @action = attributes[:action]
13
+ @params = attributes[:params]
14
+
15
+ if @params
16
+ @params_json = @params.to_json
17
+ end
18
+
19
+ @format = attributes[:format]
20
+ end
21
+
22
+ def message
23
+ message = "Processing by #{controller}##{action}"
24
+ if !message.nil?
25
+ message << " as #{format}"
26
+ end
27
+ if !params.nil? && params.length > 0
28
+ message << "\n Parameters: #{params.inspect}"
29
+ end
30
+ message
31
+ end
32
+
33
+ def to_hash
34
+ {
35
+ controller_called: Util::NonNilHashBuilder.build do |h|
36
+ h.add(:controller, controller)
37
+ h.add(:action, action)
38
+ h.add(:params_json, params_json)
39
+ end
40
+ }
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,40 @@
1
+ require "logtail/util"
2
+ require "logtail/event"
3
+
4
+ module Logtail
5
+ module Events
6
+ # @private
7
+ class Error < Logtail::Event
8
+ attr_reader :name, :error_message, :backtrace_json
9
+
10
+ def initialize(attributes)
11
+ @name = attributes[:name]
12
+ @error_message = attributes[:error_message]
13
+
14
+ if attributes[:backtrace]
15
+ @backtrace_json = attributes[:backtrace].to_json
16
+ end
17
+ end
18
+
19
+ def message
20
+ message = "#{name}"
21
+
22
+ if !error_message.nil?
23
+ message << " (#{error_message})"
24
+ end
25
+
26
+ message
27
+ end
28
+
29
+ def to_hash
30
+ {
31
+ error: {
32
+ name: name,
33
+ message: error_message,
34
+ backtrace_json: backtrace_json
35
+ }
36
+ }
37
+ end
38
+ end
39
+ end
40
+ end