rails_loki_exporter 1.0.2

Sign up to get free protection for your applications and to get access to all the features.
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: []