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 +7 -0
- data/Gemfile +3 -0
- data/README.md +61 -0
- data/lib/rails_loki_exporter/client.rb +92 -0
- data/lib/rails_loki_exporter/connection.rb +60 -0
- data/lib/rails_loki_exporter/custom_log_subscriber.rb +35 -0
- data/lib/rails_loki_exporter/intercepting_logger.rb +77 -0
- data/lib/rails_loki_exporter/my_connection.rb +8 -0
- data/lib/rails_loki_exporter/query.rb +8 -0
- data/lib/rails_loki_exporter/version.rb +3 -0
- data/lib/rails_loki_exporter.rb +46 -0
- data/rails_loki_exporter.gemspec +21 -0
- metadata +84 -0
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
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,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: []
|