logtail-ruby 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (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-ruby.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
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