rails_loki_exporter 1.0.2

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: bf716f8af36e10dc3e446c2dc7e044835927161c00cca433f666ef4c34afcd50
4
+ data.tar.gz: fd8d98a489d8123d88e5e28b048d33fedd5d069e3d88e884ab0e1ef5a4e8ff5c
5
+ SHA512:
6
+ metadata.gz: b0e41dfe931cefead14aa113b5c35268757a43d517ba296ba48b410612b3f03721808d67896f64ca187cb5d36f9797c16152b2e9f50bdeef814913b4f6163cf0
7
+ data.tar.gz: 2a949bbe2ef8a94c40c5096992dea844598e043a20dba564f2aa43e86030cd2a0ede0ce34cbcfe1e2def25701a81c74bdedc0e6fee1f9a7df9f3c9a4e7c02ad8
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gemspec
data/README.md ADDED
@@ -0,0 +1,61 @@
1
+ # Rails Loki Exporter
2
+
3
+ :gem: **Rails Loki Exporter** :gem: is a simple log epxporter for Rails.
4
+
5
+ Export logs for your Rails application to Loki instance and access them through Grafana dashboard.
6
+ ## Prerequisites
7
+ :exclamation: Before you start make sure you set up the following:
8
+ - Grafana Dashboard
9
+ - Loki Server
10
+
11
+
12
+ ## Installation
13
+
14
+ **Rails Loki Exporter**'s installation is pretty straightforward.
15
+
16
+ Using Bundler:
17
+ - Add a line for **Rails Loki Exporter** gem in your Rails application `Gemfile`:
18
+ ```rb
19
+ ...
20
+ gem 'rails_loki_exporter', '~> <version>'
21
+ ...
22
+ ```
23
+ - Install dependencies using `bundler`:
24
+ ```sh
25
+ $ bundle install
26
+ ```
27
+ - In your Rails application create `config/config.yml` file:
28
+ ```
29
+ auth_enabled: true
30
+ base_url: 'Your grafana loki url'
31
+ user_name: 'Your User number'
32
+ password: 'Your Grafana.com API Token'
33
+ log_file_path: 'log/#{Rails.env}.log'
34
+ logs_type: '%w(ERROR WARN FATAL INFO DEBUG)'
35
+ intercept_logs: true
36
+ ```
37
+ - Add block for **Rails Loki Exporter** in your `application.rb` file:
38
+ ```
39
+ require 'ruby_for_grafana_loki'
40
+ ...
41
+ ...
42
+ ...
43
+
44
+ config.after_initialize do
45
+ config_file_path = File.join(Rails.root, 'config', 'config.yml')
46
+ logger = RailsLokiExporters.create_logger(config_file_path)
47
+ Rails.logger = logger
48
+ end
49
+ ```
50
+ - Start your Rails application:
51
+ ```sh
52
+ $ rails s
53
+ ```
54
+
55
+ ## Deployment
56
+
57
+ Add additional notes about how to deploy this on a production system.
58
+
59
+ ## Resources
60
+
61
+ Add links to external resources for this project, such as CI server, bug tracker, etc.
@@ -0,0 +1,92 @@
1
+ require 'socket'
2
+ module RailsLokiExporter
3
+ LOGS_TYPE = %w(ERROR WARN FATAL INFO DEBUG).freeze
4
+
5
+ class Client
6
+ include RailsLokiExporter::Connection
7
+
8
+ attr_accessor :job_name
9
+ attr_accessor :host_name
10
+ attr_accessor :max_buffer_size
11
+ attr_accessor :interaction_interval
12
+ attr_accessor :connection
13
+
14
+ def initialize(config)
15
+ @base_url = config['base_url']
16
+ @log_file_path = config['log_file_path']
17
+ @logs_type = config['logs_type']
18
+ @intercept_logs = config['intercept_logs']
19
+ @job_name = config['job_name'] || "#{$0}_#{Process.pid}"
20
+ @host_name = config['host_name'] || Socket.gethostname
21
+ @log_buffer = []
22
+ @last_interaction_time = nil
23
+ @interaction_interval = 1 # in seconds, adjust as needed
24
+ @max_buffer_size = 10 # set the maximum number of logs to buffer
25
+ @connection = connection
26
+ end
27
+
28
+ def send_all_logs
29
+ File.open(@log_file_path, 'r') do |file|
30
+ file.each_line do |line|
31
+ send_log(line)
32
+ end
33
+ end
34
+ end
35
+
36
+ def send_log(log_message)
37
+ @log_buffer << log_message
38
+ if @log_buffer.size >= @max_buffer_size || can_send_log?
39
+ send_buffered_logs
40
+ @last_interaction_time = Time.now
41
+ else
42
+ # @logger.info('Log buffered. Waiting for more logs or interaction interval.')
43
+ end
44
+ end
45
+
46
+ private
47
+ def send_buffered_logs
48
+ return if @log_buffer.empty?
49
+
50
+ curr_datetime = Time.now.to_i * 1_000_000_000
51
+ msg = "On server #{@host_name} detected error"
52
+ payload = {
53
+ 'streams' => [
54
+ {
55
+ 'stream' => {
56
+ 'job' => @job_name,
57
+ 'host' => @host_name
58
+ },
59
+ 'values' => @log_buffer.map { |log| [curr_datetime.to_s, log] },
60
+ 'entries' => @log_buffer.map do |_|
61
+ {
62
+ 'ts' => curr_datetime,
63
+ 'line' => "[WARN] " + msg
64
+ }
65
+ end
66
+ }
67
+ ]
68
+ }
69
+
70
+ json_payload = JSON.generate(payload)
71
+ uri = '/loki/api/v1/push'
72
+ @connection.post(uri, json_payload)
73
+
74
+ @log_buffer.clear
75
+ end
76
+
77
+ def can_send_log?
78
+ return true if @last_interaction_time.nil?
79
+
80
+ elapsed_time = Time.now - @last_interaction_time
81
+ elapsed_time >= @interaction_interval
82
+ end
83
+
84
+ def match_logs_type?(log_line)
85
+ return false if log_line.nil?
86
+
87
+ type_match = log_line.match(/(ERROR|WARN|FATAL|INFO|DEBUG)/)
88
+ type = type_match ? type_match.to_s : 'UNMATCHED'
89
+ type == 'UNMATCHED' || @logs_type.include?(type)
90
+ end
91
+ end
92
+ end
@@ -0,0 +1,60 @@
1
+ require 'json'
2
+ require 'net/http'
3
+ require 'uri'
4
+
5
+ module RailsLokiExporter
6
+ module Connection
7
+ def initialize(base_url, user_name, password, auth_enabled, host_name, job_name)
8
+ uri = URI.parse(base_url)
9
+ @base_url = uri
10
+ @user_name = user_name
11
+ @password = password
12
+ @auth_enabled = auth_enabled
13
+ end
14
+ def connection
15
+ http = Net::HTTP.new(@base_url.to_s, @base_url.port)
16
+ http.use_ssl = @base_url.scheme == 'https'
17
+ http.read_timeout = 30 # Adjust as needed
18
+ http.open_timeout = 30 # Adjust as needed
19
+ http
20
+ end
21
+ def post(url_loki, body)
22
+ url = @base_url.to_s + url_loki
23
+ username = @user_name
24
+ password = @password
25
+ send_authenticated_post(url, body, username, password)
26
+ end
27
+ def send_authenticated_post(url, body, username, password)
28
+ uri = URI.parse(url)
29
+ request = Net::HTTP::Post.new(uri.path)
30
+ request['Content-Type'] = 'application/json'
31
+ request.body = body
32
+
33
+ if username && password && @auth_enabled
34
+ request.basic_auth(username, password)
35
+ else
36
+ raise "Username or password is nil."
37
+ end
38
+
39
+ response = Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == 'https') do |http|
40
+ http.request(request)
41
+ end
42
+
43
+ case response
44
+ when Net::HTTPSuccess
45
+ response.body ? JSON.parse(response.body) : nil
46
+ when Net::HTTPNoContent
47
+ puts "Request successful, but no content was returned."
48
+ nil
49
+ else
50
+ raise "Failed to make POST request. Response code: #{response.code}, Response body: #{response.body}"
51
+ end
52
+ rescue StandardError => e
53
+ puts "Error: #{e.message}"
54
+ end
55
+ def base64_encode_credentials(user_name, password)
56
+ credentials = "#{user_name}:#{password}"
57
+ Base64.strict_encode64(credentials)
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,35 @@
1
+
2
+ require 'action_controller/log_subscriber'
3
+
4
+ module RailsLokiExporter
5
+ class CustomLogSubscriber < ActiveSupport::LogSubscriber
6
+ INTERNAL_PARAMS = %w(controller action format _method only_path)
7
+ class << self
8
+ attr_accessor :client
9
+ end
10
+
11
+ def process_action(event)
12
+ if self.class.client
13
+
14
+ payload = event.payload
15
+ controller = payload[:controller]
16
+ action = payload[:action]
17
+ status = payload[:status]
18
+ duration = event.duration.round(2)
19
+ path = payload[:path]
20
+ params = payload[:params].except(*INTERNAL_PARAMS)
21
+ query_string = params.to_query
22
+
23
+ request_url = query_string.blank? ? path : "#{path}?#{query_string}"
24
+ log_message = "LogSubscriber - [#{controller}##{action}] URL:#{request_url}, Params: #{params}, Status: #{status}, Duration: #{duration}ms"
25
+ self.class.client.send_log(log_message)
26
+ end
27
+ end
28
+ end
29
+ end
30
+
31
+ %w(process_action start_processing).each do |evt|
32
+ ActiveSupport::Notifications.unsubscribe "#{evt}.action_controller"
33
+ end
34
+
35
+ RailsLokiExporter::CustomLogSubscriber .attach_to :action_controller
@@ -0,0 +1,77 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+ require 'net/http'
5
+ require 'active_support/logger'
6
+
7
+ module RailsLokiExporter
8
+ class InterceptingLogger < ActiveSupport::Logger
9
+ attr_accessor :client
10
+
11
+ SEVERITY_NAMES = %w(DEBUG INFO WARN ERROR FATAL).freeze
12
+
13
+ def initialize(intercept_logs: false)
14
+ @intercept_logs = intercept_logs
15
+ @log = ""
16
+ super(STDOUT)
17
+ self.level = Logger::DEBUG
18
+ end
19
+
20
+ def add(severity, message = nil, progname = nil, &block)
21
+ severity_name = severity_name(severity)
22
+ log_message = message
23
+ if log_message.nil?
24
+ if block_given?
25
+ log_message = yield
26
+ end
27
+ end
28
+
29
+ if @intercept_logs
30
+ if log_message.nil?
31
+ puts caller
32
+ else
33
+ client.send_log(@log) if client
34
+ end
35
+ end
36
+ super(severity, message, progname, &block)
37
+ end
38
+
39
+ def debug(log_message = "")
40
+ client.send_log("#{log_message}") if client
41
+ end
42
+
43
+ def info(log_message = "")
44
+ client.send_log("#{log_message}") if client
45
+ end
46
+
47
+ def fatal(log_message = "")
48
+ client.send_log("#{log_message}") if client
49
+ end
50
+
51
+ def warn(log_message = "")
52
+ client.send_log("#{log_message}") if client
53
+ end
54
+
55
+ def error(log_message = "")
56
+ client.send_log("#{log_message}") if client
57
+ end
58
+
59
+ def broadcast_to(console)
60
+ client.send_log(@log) if client
61
+ end
62
+
63
+ private
64
+
65
+ def format_message(severity, datetime, progname, msg)
66
+ puts "severity: #{severity}"
67
+ puts "datetime: #{datetime}"
68
+ puts "progname: #{progname}"
69
+ puts "msg: #{msg}"
70
+ "#{severity} #{progname}: #{msg}\n"
71
+ end
72
+
73
+ def severity_name(severity)
74
+ SEVERITY_NAMES[severity] || "UNKNOWN"
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,8 @@
1
+ module RailsLokiExporter
2
+ class MyConnection
3
+ include Connection
4
+ def self.create(base_url, user_name, password, auth_enabled, host_name, job_name)
5
+ new(base_url, user_name, password, auth_enabled, host_name, job_name).connection
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,8 @@
1
+ module RailsLokiExporter
2
+ class Query
3
+ attr_reader :streams_data
4
+ def initialize(response)
5
+ @streams_data = response['streams']
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,3 @@
1
+ module RailsLokiExporter
2
+ VERSION = '1.0.2'
3
+ end
@@ -0,0 +1,46 @@
1
+ require 'zeitwerk'
2
+
3
+ loader = Zeitwerk::Loader.for_gem
4
+ loader.setup
5
+
6
+ module RailsLokiExporter
7
+ class << self
8
+ def create_logger(config_file_path)
9
+ config = load_config(config_file_path)
10
+
11
+ connection_instance = MyConnection.new(
12
+ config['base_url'],
13
+ config['user_name'],
14
+ config['password'],
15
+ config['auth_enabled'],
16
+ config['host_name'],
17
+ config['job_name']
18
+ )
19
+
20
+ client = Client.new(config)
21
+ logger = InterceptingLogger.new(intercept_logs: config['intercept_logs'])
22
+ if config['enable_log_subscriber']
23
+ CustomLogSubscriber.client = client
24
+ end
25
+ logger.client = client
26
+ client.connection = connection_instance
27
+ logger
28
+ end
29
+
30
+ private
31
+
32
+ def load_config(config_file_path)
33
+ expanded_path = File.expand_path(config_file_path, __dir__)
34
+
35
+ if File.exist?(expanded_path)
36
+ config_erb = ERB.new(File.read(expanded_path)).result
37
+ config = YAML.safe_load(config_erb, aliases: true)
38
+ puts config.to_json
39
+ return config
40
+ else
41
+ puts "Config file not found: #{expanded_path}"
42
+ return {}
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,21 @@
1
+ require File.expand_path('lib/rails_loki_exporter/version', __dir__)
2
+
3
+ Gem::Specification.new do |spec|
4
+ spec.name = 'rails_loki_exporter'
5
+ spec.version = RailsLokiExporter::VERSION
6
+ spec.authors = ['Oleg Ten', 'Assiya Kalykova']
7
+ spec.email = ['tennet0505@gmail.com']
8
+ spec.summary = 'Ruby for Grafana Loki'
9
+ spec.description = 'Attempt to make gem'
10
+ spec.homepage = 'https://rubygems.org/gems/hello_app_ak_gem'
11
+ spec.license = 'MIT'
12
+ spec.platform = Gem::Platform::RUBY
13
+ spec.required_ruby_version = '>= 2.7.0'
14
+ spec.files = Dir['README.md', 'lib/**/*.rb',
15
+ 'rails_loki_exporter.gemspec',
16
+ 'Gemfile']
17
+ spec.extra_rdoc_files = ['README.md']
18
+ spec.require_paths = ['lib']
19
+ spec.add_dependency 'zeitwerk', '~> 2.4'
20
+ spec.add_dependency 'rspec'
21
+ end
metadata ADDED
@@ -0,0 +1,84 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: rails_loki_exporter
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.2
5
+ platform: ruby
6
+ authors:
7
+ - Oleg Ten
8
+ - Assiya Kalykova
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2024-02-02 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: zeitwerk
16
+ requirement: !ruby/object:Gem::Requirement
17
+ requirements:
18
+ - - "~>"
19
+ - !ruby/object:Gem::Version
20
+ version: '2.4'
21
+ type: :runtime
22
+ prerelease: false
23
+ version_requirements: !ruby/object:Gem::Requirement
24
+ requirements:
25
+ - - "~>"
26
+ - !ruby/object:Gem::Version
27
+ version: '2.4'
28
+ - !ruby/object:Gem::Dependency
29
+ name: rspec
30
+ requirement: !ruby/object:Gem::Requirement
31
+ requirements:
32
+ - - ">="
33
+ - !ruby/object:Gem::Version
34
+ version: '0'
35
+ type: :runtime
36
+ prerelease: false
37
+ version_requirements: !ruby/object:Gem::Requirement
38
+ requirements:
39
+ - - ">="
40
+ - !ruby/object:Gem::Version
41
+ version: '0'
42
+ description: Attempt to make gem
43
+ email:
44
+ - tennet0505@gmail.com
45
+ executables: []
46
+ extensions: []
47
+ extra_rdoc_files:
48
+ - README.md
49
+ files:
50
+ - Gemfile
51
+ - README.md
52
+ - lib/rails_loki_exporter.rb
53
+ - lib/rails_loki_exporter/client.rb
54
+ - lib/rails_loki_exporter/connection.rb
55
+ - lib/rails_loki_exporter/custom_log_subscriber.rb
56
+ - lib/rails_loki_exporter/intercepting_logger.rb
57
+ - lib/rails_loki_exporter/my_connection.rb
58
+ - lib/rails_loki_exporter/query.rb
59
+ - lib/rails_loki_exporter/version.rb
60
+ - rails_loki_exporter.gemspec
61
+ homepage: https://rubygems.org/gems/hello_app_ak_gem
62
+ licenses:
63
+ - MIT
64
+ metadata: {}
65
+ post_install_message:
66
+ rdoc_options: []
67
+ require_paths:
68
+ - lib
69
+ required_ruby_version: !ruby/object:Gem::Requirement
70
+ requirements:
71
+ - - ">="
72
+ - !ruby/object:Gem::Version
73
+ version: 2.7.0
74
+ required_rubygems_version: !ruby/object:Gem::Requirement
75
+ requirements:
76
+ - - ">="
77
+ - !ruby/object:Gem::Version
78
+ version: '0'
79
+ requirements: []
80
+ rubygems_version: 3.1.6
81
+ signing_key:
82
+ specification_version: 4
83
+ summary: Ruby for Grafana Loki
84
+ test_files: []