logister-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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 6f3dcdd4ed71335923f5b4fd3d77dfa4af74a4d958715eaaf0805d704739f1a4
4
+ data.tar.gz: 147df51a94bea770c1559f0186eb74d154cd7a6b109a3b4025496ca8043abd68
5
+ SHA512:
6
+ metadata.gz: d1328c3bb4188794e62215e0e2fd7e0c50631d0d1b3c92fbe3fec5dc179510109cfb95b43629f588f74ef8efe790d4c190db494d4ab4527b9229701cf9c7bddf
7
+ data.tar.gz: 8e700cdfbf1ff0b1f0178c260270170f448165f6043e04bca8f7c6ef5365254819ce223db9adcb40cdd12b7f1ce7d6f11212921f2dd2bc73edd2f70dba7af82e
data/README.md ADDED
@@ -0,0 +1,70 @@
1
+ # logister-ruby
2
+
3
+ `logister-ruby` sends application errors and custom metrics to `logister.org`.
4
+
5
+ ## Install
6
+
7
+ ```ruby
8
+ gem "logister-ruby"
9
+ ```
10
+
11
+ Then generate an initializer in Rails:
12
+
13
+ ```bash
14
+ bin/rails generate logister:install
15
+ ```
16
+
17
+ ## Configuration
18
+
19
+ ```ruby
20
+ Logister.configure do |config|
21
+ config.api_key = ENV.fetch("LOGISTER_API_KEY")
22
+ config.endpoint = "https://logister.org/api/v1/ingest_events"
23
+ config.environment = Rails.env
24
+ config.service = Rails.application.class.module_parent_name.underscore
25
+ config.release = ENV["RELEASE_SHA"]
26
+ end
27
+ ```
28
+
29
+ ## Reliability options
30
+
31
+ ```ruby
32
+ Logister.configure do |config|
33
+ config.async = true
34
+ config.queue_size = 1000
35
+ config.max_retries = 3
36
+ config.retry_base_interval = 0.5
37
+ end
38
+ ```
39
+
40
+ ## Filtering and redaction
41
+
42
+ ```ruby
43
+ Logister.configure do |config|
44
+ config.ignore_environments = ["development", "test"]
45
+ config.ignore_exceptions = ["ActiveRecord::RecordNotFound"]
46
+ config.ignore_paths = [/health/, "/up"]
47
+
48
+ config.before_notify = lambda do |payload|
49
+ payload[:context]&.delete("authorization")
50
+ payload
51
+ end
52
+ end
53
+ ```
54
+
55
+ ## Rails auto-reporting
56
+
57
+ If Rails is present, the gem installs middleware that reports unhandled exceptions automatically.
58
+
59
+ ## Manual reporting
60
+
61
+ ```ruby
62
+ Logister.report_error(StandardError.new("Something failed"), tags: { area: "checkout" })
63
+
64
+ Logister.report_metric(
65
+ message: "checkout.completed",
66
+ level: "info",
67
+ context: { duration_ms: 123 },
68
+ tags: { region: "us-east-1" }
69
+ )
70
+ ```
@@ -0,0 +1,13 @@
1
+ require 'rails/generators'
2
+
3
+ module Logister
4
+ module Generators
5
+ class InstallGenerator < Rails::Generators::Base
6
+ source_root File.expand_path('templates', __dir__)
7
+
8
+ def create_initializer
9
+ template 'logister.rb', 'config/initializers/logister.rb'
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,23 @@
1
+ Logister.configure do |config|
2
+ config.api_key = ENV['LOGISTER_API_KEY']
3
+ config.endpoint = ENV.fetch('LOGISTER_ENDPOINT', 'https://logister.org/api/v1/ingest_events')
4
+ config.environment = Rails.env
5
+ config.service = Rails.application.class.module_parent_name.underscore
6
+ config.release = ENV['LOGISTER_RELEASE']
7
+
8
+ config.enabled = true
9
+ config.timeout_seconds = 2
10
+
11
+ config.async = true
12
+ config.queue_size = 1000
13
+ config.max_retries = 3
14
+ config.retry_base_interval = 0.5
15
+
16
+ config.ignore_environments = []
17
+ config.ignore_exceptions = []
18
+ config.ignore_paths = []
19
+
20
+ config.before_notify = lambda do |payload|
21
+ payload
22
+ end
23
+ end
@@ -0,0 +1,127 @@
1
+ require 'json'
2
+ require 'net/http'
3
+ require 'uri'
4
+
5
+ module Logister
6
+ class Client
7
+ def initialize(configuration)
8
+ @configuration = configuration
9
+ @worker_mutex = Mutex.new
10
+ @queue = SizedQueue.new(@configuration.queue_size)
11
+ @worker = nil
12
+ @running = false
13
+ end
14
+
15
+ def publish(payload)
16
+ return false unless ready?
17
+
18
+ return publish_sync(payload) unless @configuration.async
19
+
20
+ ensure_worker_started
21
+ enqueue(payload)
22
+ end
23
+
24
+ def flush(timeout: 2)
25
+ return true unless @configuration.async
26
+
27
+ started_at = monotonic_now
28
+ while @queue.length.positive?
29
+ return false if monotonic_now - started_at > timeout
30
+
31
+ sleep(0.01)
32
+ end
33
+
34
+ true
35
+ end
36
+
37
+ def shutdown
38
+ return true unless @configuration.async
39
+
40
+ @running = false
41
+ begin
42
+ @queue.push(nil)
43
+ rescue StandardError
44
+ nil
45
+ end
46
+ @worker&.join(1)
47
+ true
48
+ end
49
+
50
+ private
51
+
52
+ def enqueue(payload)
53
+ @queue.push(payload, true)
54
+ true
55
+ rescue ThreadError
56
+ @configuration.logger.warn('logister queue full; dropping event')
57
+ false
58
+ end
59
+
60
+ def ensure_worker_started
61
+ return if @running && @worker&.alive?
62
+
63
+ @worker_mutex.synchronize do
64
+ return if @running && @worker&.alive?
65
+
66
+ @running = true
67
+ @worker = Thread.new { run_worker }
68
+ end
69
+ end
70
+
71
+ def run_worker
72
+ while @running
73
+ payload = @queue.pop
74
+ break if payload.nil?
75
+
76
+ publish_sync(payload)
77
+ end
78
+ rescue StandardError => e
79
+ @configuration.logger.warn("logister worker crashed: #{e.class} #{e.message}")
80
+ @running = false
81
+ end
82
+
83
+ def publish_sync(payload)
84
+ attempts = 0
85
+ begin
86
+ attempts += 1
87
+ send_request(payload)
88
+ rescue StandardError => e
89
+ if attempts <= @configuration.max_retries
90
+ sleep(@configuration.retry_base_interval * (2**(attempts - 1)))
91
+ retry
92
+ end
93
+
94
+ @configuration.logger.warn("logister publish failed: #{e.class} #{e.message}")
95
+ false
96
+ end
97
+ end
98
+
99
+ def send_request(payload)
100
+ uri = URI.parse(@configuration.endpoint)
101
+ request = Net::HTTP::Post.new(uri)
102
+ request['Content-Type'] = 'application/json'
103
+ request['Authorization'] = "Bearer #{@configuration.api_key}"
104
+ request.body = { event: payload }.to_json
105
+
106
+ response = Net::HTTP.start(
107
+ uri.host,
108
+ uri.port,
109
+ use_ssl: uri.scheme == 'https',
110
+ open_timeout: @configuration.timeout_seconds,
111
+ read_timeout: @configuration.timeout_seconds
112
+ ) { |http| http.request(request) }
113
+
114
+ return true if response.is_a?(Net::HTTPSuccess)
115
+
116
+ raise "HTTP #{response.code}"
117
+ end
118
+
119
+ def monotonic_now
120
+ Process.clock_gettime(Process::CLOCK_MONOTONIC)
121
+ end
122
+
123
+ def ready?
124
+ @configuration.enabled && @configuration.api_key.to_s != ''
125
+ end
126
+ end
127
+ end
@@ -0,0 +1,31 @@
1
+ require 'logger'
2
+
3
+ module Logister
4
+ class Configuration
5
+ attr_accessor :api_key, :endpoint, :environment, :service, :release, :enabled, :timeout_seconds, :logger,
6
+ :ignore_exceptions, :ignore_environments, :ignore_paths, :before_notify,
7
+ :async, :queue_size, :max_retries, :retry_base_interval
8
+
9
+ def initialize
10
+ @api_key = ENV['LOGISTER_API_KEY']
11
+ @endpoint = ENV.fetch('LOGISTER_ENDPOINT', 'https://logister.org/api/v1/ingest_events')
12
+ @environment = ENV.fetch('RAILS_ENV', ENV.fetch('RACK_ENV', 'development'))
13
+ @service = ENV.fetch('LOGISTER_SERVICE', 'ruby-app')
14
+ @release = ENV['LOGISTER_RELEASE']
15
+ @enabled = true
16
+ @timeout_seconds = 2
17
+ @logger = Logger.new($stdout)
18
+ @logger.level = Logger::WARN
19
+
20
+ @ignore_exceptions = []
21
+ @ignore_environments = []
22
+ @ignore_paths = []
23
+ @before_notify = nil
24
+
25
+ @async = true
26
+ @queue_size = 1000
27
+ @max_retries = 3
28
+ @retry_base_interval = 0.5
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,21 @@
1
+ module Logister
2
+ class Middleware
3
+ def initialize(app)
4
+ @app = app
5
+ end
6
+
7
+ def call(env)
8
+ @app.call(env)
9
+ rescue StandardError => e
10
+ Logister.report_error(
11
+ e,
12
+ context: {
13
+ request_id: env['action_dispatch.request_id'],
14
+ path: env['PATH_INFO'],
15
+ method: env['REQUEST_METHOD']
16
+ }
17
+ )
18
+ raise
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,40 @@
1
+ require 'rails/railtie'
2
+
3
+ module Logister
4
+ class Railtie < Rails::Railtie
5
+ config.logister = ActiveSupport::OrderedOptions.new
6
+
7
+ initializer 'logister.configure' do |app|
8
+ Logister.configure do |config|
9
+ copy_setting(app, config, :api_key)
10
+ copy_setting(app, config, :endpoint)
11
+ copy_setting(app, config, :environment)
12
+ copy_setting(app, config, :service)
13
+ copy_setting(app, config, :release)
14
+ copy_setting(app, config, :enabled)
15
+ copy_setting(app, config, :timeout_seconds)
16
+ copy_setting(app, config, :ignore_exceptions)
17
+ copy_setting(app, config, :ignore_environments)
18
+ copy_setting(app, config, :ignore_paths)
19
+ copy_setting(app, config, :before_notify)
20
+ copy_setting(app, config, :async)
21
+ copy_setting(app, config, :queue_size)
22
+ copy_setting(app, config, :max_retries)
23
+ copy_setting(app, config, :retry_base_interval)
24
+ end
25
+ end
26
+
27
+ initializer 'logister.middleware' do |app|
28
+ app.middleware.use Logister::Middleware
29
+ end
30
+
31
+ private
32
+
33
+ def copy_setting(app, config, key)
34
+ value = app.config.logister.send(key)
35
+ return if value.nil?
36
+
37
+ config.send("#{key}=", value)
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,124 @@
1
+ require 'digest'
2
+ require 'time'
3
+
4
+ module Logister
5
+ class Reporter
6
+ def initialize(configuration)
7
+ @configuration = configuration
8
+ @client = Client.new(configuration)
9
+
10
+ at_exit { shutdown }
11
+ end
12
+
13
+ def report_error(exception, context: {}, tags: {}, level: 'error', fingerprint: nil)
14
+ return false if ignored_exception?(exception)
15
+ return false if ignored_path?(context)
16
+
17
+ payload = build_payload(
18
+ event_type: 'error',
19
+ level: level,
20
+ message: "#{exception.class}: #{exception.message}",
21
+ fingerprint: fingerprint || default_fingerprint(exception),
22
+ context: context.merge(
23
+ exception: {
24
+ class: exception.class.to_s,
25
+ message: exception.message.to_s,
26
+ backtrace: Array(exception.backtrace).first(50)
27
+ },
28
+ tags: tags
29
+ )
30
+ )
31
+
32
+ payload = apply_before_notify(payload)
33
+ return false unless payload
34
+
35
+ @client.publish(payload)
36
+ end
37
+
38
+ def report_metric(message:, level: 'info', context: {}, tags: {}, fingerprint: nil)
39
+ return false if ignored_environment?
40
+ return false if ignored_path?(context)
41
+
42
+ payload = build_payload(
43
+ event_type: 'metric',
44
+ level: level,
45
+ message: message,
46
+ fingerprint: fingerprint || Digest::SHA256.hexdigest(message.to_s)[0, 32],
47
+ context: context.merge(tags: tags)
48
+ )
49
+
50
+ payload = apply_before_notify(payload)
51
+ return false unless payload
52
+
53
+ @client.publish(payload)
54
+ end
55
+
56
+ def flush(timeout: 2)
57
+ @client.flush(timeout: timeout)
58
+ end
59
+
60
+ def shutdown
61
+ @client.shutdown
62
+ end
63
+
64
+ private
65
+
66
+ def build_payload(event_type:, level:, message:, fingerprint:, context:)
67
+ {
68
+ event_type: event_type,
69
+ level: level,
70
+ message: message,
71
+ fingerprint: fingerprint,
72
+ occurred_at: Time.now.utc.iso8601,
73
+ context: context.merge(
74
+ environment: @configuration.environment,
75
+ service: @configuration.service,
76
+ release: @configuration.release
77
+ )
78
+ }
79
+ end
80
+
81
+ def apply_before_notify(payload)
82
+ hook = @configuration.before_notify
83
+ return payload unless hook.respond_to?(:call)
84
+
85
+ result = hook.call(payload)
86
+ return nil if result == false || result.nil?
87
+
88
+ result
89
+ rescue StandardError => e
90
+ @configuration.logger.warn("logister before_notify failed: #{e.class} #{e.message}")
91
+ nil
92
+ end
93
+
94
+ def ignored_exception?(exception)
95
+ return true if ignored_environment?
96
+
97
+ @configuration.ignore_exceptions.any? do |item|
98
+ if item.is_a?(Class)
99
+ exception.is_a?(item)
100
+ else
101
+ exception.class.name == item.to_s
102
+ end
103
+ end
104
+ end
105
+
106
+ def ignored_environment?
107
+ env = @configuration.environment.to_s
108
+ @configuration.ignore_environments.map(&:to_s).include?(env)
109
+ end
110
+
111
+ def ignored_path?(context)
112
+ path = context[:path] || context['path']
113
+ return false if path.to_s.empty?
114
+
115
+ @configuration.ignore_paths.any? do |matcher|
116
+ matcher.is_a?(Regexp) ? matcher.match?(path.to_s) : path.to_s.include?(matcher.to_s)
117
+ end
118
+ end
119
+
120
+ def default_fingerprint(exception)
121
+ Digest::SHA256.hexdigest("#{exception.class}|#{exception.message}")[0, 32]
122
+ end
123
+ end
124
+ end
@@ -0,0 +1,3 @@
1
+ module Logister
2
+ VERSION = '0.1.0'
3
+ end
data/lib/logister.rb ADDED
@@ -0,0 +1,40 @@
1
+ require_relative 'logister/version'
2
+ require_relative 'logister/configuration'
3
+ require_relative 'logister/client'
4
+ require_relative 'logister/reporter'
5
+ require_relative 'logister/middleware'
6
+
7
+ module Logister
8
+ class << self
9
+ def configuration
10
+ @configuration ||= Configuration.new
11
+ end
12
+
13
+ def configure
14
+ yield(configuration)
15
+ @reporter = nil
16
+ end
17
+
18
+ def reporter
19
+ @reporter ||= Reporter.new(configuration)
20
+ end
21
+
22
+ def report_error(exception, **kwargs)
23
+ reporter.report_error(exception, **kwargs)
24
+ end
25
+
26
+ def report_metric(**kwargs)
27
+ reporter.report_metric(**kwargs)
28
+ end
29
+
30
+ def flush(timeout: 2)
31
+ reporter.flush(timeout: timeout)
32
+ end
33
+
34
+ def shutdown
35
+ reporter.shutdown
36
+ end
37
+ end
38
+ end
39
+
40
+ require_relative 'logister/railtie' if defined?(Rails::Railtie)
@@ -0,0 +1,25 @@
1
+ require_relative 'lib/logister/version'
2
+
3
+ Gem::Specification.new do |spec|
4
+ spec.name = 'logister-ruby'
5
+ spec.version = Logister::VERSION
6
+ spec.authors = ['Logister']
7
+ spec.email = ['support@logister.org']
8
+
9
+ spec.summary = 'Send Rails errors and metrics to logister.org'
10
+ spec.description = 'Client gem for reporting errors and custom metrics from Ruby and Rails apps to logister.org'
11
+ spec.homepage = 'https://logister.org'
12
+ spec.license = 'MIT'
13
+ spec.required_ruby_version = '>= 3.1.0'
14
+
15
+ spec.metadata['homepage_uri'] = spec.homepage
16
+ spec.metadata['source_code_uri'] = 'https://logister.org'
17
+
18
+ spec.files = Dir.chdir(__dir__) do
19
+ Dir.glob('lib/**/*') + ['README.md', 'logister-ruby.gemspec']
20
+ end
21
+
22
+ spec.require_paths = ['lib']
23
+
24
+ spec.add_dependency 'activesupport', '>= 6.1'
25
+ end
metadata ADDED
@@ -0,0 +1,68 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: logister-ruby
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Logister
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: activesupport
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - ">="
17
+ - !ruby/object:Gem::Version
18
+ version: '6.1'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - ">="
24
+ - !ruby/object:Gem::Version
25
+ version: '6.1'
26
+ description: Client gem for reporting errors and custom metrics from Ruby and Rails
27
+ apps to logister.org
28
+ email:
29
+ - support@logister.org
30
+ executables: []
31
+ extensions: []
32
+ extra_rdoc_files: []
33
+ files:
34
+ - README.md
35
+ - lib/generators/logister/install_generator.rb
36
+ - lib/generators/logister/templates/logister.rb
37
+ - lib/logister.rb
38
+ - lib/logister/client.rb
39
+ - lib/logister/configuration.rb
40
+ - lib/logister/middleware.rb
41
+ - lib/logister/railtie.rb
42
+ - lib/logister/reporter.rb
43
+ - lib/logister/version.rb
44
+ - logister-ruby.gemspec
45
+ homepage: https://logister.org
46
+ licenses:
47
+ - MIT
48
+ metadata:
49
+ homepage_uri: https://logister.org
50
+ source_code_uri: https://logister.org
51
+ rdoc_options: []
52
+ require_paths:
53
+ - lib
54
+ required_ruby_version: !ruby/object:Gem::Requirement
55
+ requirements:
56
+ - - ">="
57
+ - !ruby/object:Gem::Version
58
+ version: 3.1.0
59
+ required_rubygems_version: !ruby/object:Gem::Requirement
60
+ requirements:
61
+ - - ">="
62
+ - !ruby/object:Gem::Version
63
+ version: '0'
64
+ requirements: []
65
+ rubygems_version: 4.0.3
66
+ specification_version: 4
67
+ summary: Send Rails errors and metrics to logister.org
68
+ test_files: []