strum-logs 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "singleton"
4
+ module StrumLogs
5
+ class DefaultLogger
6
+ include Singleton
7
+
8
+ attr_reader :logger
9
+
10
+ def initialize
11
+ @logger = init_log_system
12
+ end
13
+
14
+ def self.logger
15
+ instance.logger
16
+ end
17
+
18
+ private
19
+
20
+ def init_log_system # rubocop:disable Metrics/AbcSize
21
+ $stdout.sync = true if Configuration.config.stdout_sync
22
+ logger = Ougai::Logger.new($stdout)
23
+ logger.before_log = lambda do |data|
24
+ tracer_info_set(data)
25
+ end
26
+ logger.level = Configuration.config.level
27
+ logger.formatter = Ougai::Formatters::Readable.new unless %w[prod production
28
+ test].include?(Configuration.config.environment)
29
+ logger.with_fields = { service_name: Configuration.config.application_name,
30
+ version: Configuration.config.application_version }
31
+ logger
32
+ end
33
+
34
+ def tracer_info_set(data)
35
+ span = OpenTelemetry::Trace.current_span(OpenTelemetry::Context.current)
36
+ context = span.context
37
+ data[:trace_id] = context.hex_trace_id
38
+ data[:span_id] = context.hex_span_id
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "singleton"
4
+ module StrumLogs
5
+ class DefaultTelemetry
6
+ include Singleton
7
+
8
+ attr_reader :tracer
9
+
10
+ def initialize
11
+ @tracer = init_tracer
12
+ end
13
+
14
+ def self.tracer
15
+ instance.tracer
16
+ end
17
+
18
+ private
19
+
20
+ def init_tracer
21
+ open_telemetry_config
22
+ OpenTelemetry.tracer_provider.tracer(Configuration.config.application_name,
23
+ Configuration.config.application_version)
24
+ end
25
+
26
+ def open_telemetry_config # rubocop:disable Metrics/CyclomaticComplexity,Metrics/AbcSize
27
+ ENV["OTEL_TRACES_EXPORTER"] = "none" if !Configuration.config.enable_export_spans && !ENV.key?("OTEL_TRACES_EXPORTER")
28
+ OpenTelemetry::SDK.configure do |c|
29
+ c.service_name = Configuration.config.application_name
30
+ c.service_version = Configuration.config.application_version
31
+ c.use "OpenTelemetry::Instrumentation::PG" if Configuration.config.pg_instrumentation
32
+ c.use "OpenTelemetry::Instrumentation::Redis" if Configuration.config.redis_instrumentation
33
+ c.use "OpenTelemetry::Instrumentation::Rack" if Configuration.config.rack_instrumentation
34
+ c.use "OpenTelemetry::Instrumentation::Faraday" if Configuration.config.faraday_instrumentation
35
+ c.use "OpenTelemetry::Instrumentation::Bunny" if Configuration.config.rabbit_instrumentation
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module StrumLogs
4
+ module Errors
5
+ class Configuration < StandardError; end
6
+ end
7
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module StrumLogs
4
+ class EsbFormatter
5
+ def self.call(severity, time, _program_name, message)
6
+ {
7
+ level: severity,
8
+ pid: Process.pid,
9
+ message: message[:msg],
10
+ started_at: time.utc.iso8601
11
+ }.merge!(message)
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "strum_logs/configuration"
4
+
5
+ module StrumLogs
6
+ class StrumEsbHooks
7
+ class << self
8
+ def before_publish_hook(body, properties)
9
+ DefaultLogger.logger.info({ message: "Publishing AMQP message",
10
+ body: body,
11
+ properties: properties,
12
+ protocol: "AMQP" })
13
+ end
14
+
15
+ def before_handler_hook(body, properties, metadata)
16
+ DefaultLogger.logger.info({ message: "Handling AMQP message",
17
+ body: body,
18
+ properties: properties,
19
+ protocol: "AMQP",
20
+ metadata: metadata })
21
+ end
22
+
23
+ def after_handler_hook(body, properties, metadata, payload, error)
24
+ data = handler_payload(body, properties, metadata, payload)
25
+ if error
26
+ data[:message] = "Failed to process AMQP message"
27
+ data[:error] = error
28
+ data[:stack_trace] = error.backtrace if Configuration.config.stack_trace
29
+ DefaultLogger.logger.error(data)
30
+ else
31
+ DefaultLogger.logger.info(data)
32
+ end
33
+ end
34
+
35
+ private
36
+
37
+ def handler_payload(body, properties, metadata, payload)
38
+ { message: "After handling AMQP message",
39
+ body: payload || body,
40
+ protocol: "AMQP",
41
+ consumer_tag: properties[:consumer_tag],
42
+ redelivered: properties[:redelivered],
43
+ exchange: properties[:exchange],
44
+ routing_key: properties[:routing_key],
45
+ content_type: metadata[:content_type],
46
+ headers: metadata[:headers],
47
+ delivery_mode: metadata[:delivery_mode] }
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,76 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "strum_logs/default_logger"
4
+ require "strum_logs/helpers/http_log_helper"
5
+
6
+ module StrumLogs
7
+ module Faraday
8
+ class RequestLogMiddleware
9
+ include StrumLogs::Helpers::HttpLogHelper
10
+
11
+ def initialize(app)
12
+ @app = app
13
+ @logger = DefaultLogger.instance.logger
14
+ @tracer = DefaultTelemetry.instance.tracer
15
+ end
16
+
17
+ def call(request_env)
18
+ @tracer.in_span(request_env.url.to_s, kind: :client) do |span|
19
+ logging_process(request_env, span)
20
+ end
21
+ end
22
+
23
+ def logging_process(request_env, span)
24
+ OpenTelemetry.propagation.inject(request_env.request_headers)
25
+ log_entity = init_log(request_env)
26
+ @app.call(request_env).on_complete do |response_env|
27
+ tracer_info_set(response_env)
28
+ log_data(log_entity, response_env)
29
+ log_status(log_entity, response_env)
30
+ rescue StandardError => e
31
+ error_process(log_entity, e, span)
32
+ raise e
33
+ ensure
34
+ logg_process(log_entity, response_env)
35
+ end
36
+ end
37
+
38
+ def log_status(log_entity, env)
39
+ env.success? ? log_entity.merge!({ log_status: "success" }) : log_entity.merge!({ log_status: "error" })
40
+ end
41
+
42
+ def log_data(log_entity, response_env)
43
+ log_entity[:status] = response_env.status
44
+ log_entity[:headers] = response_env.response_headers
45
+ log_entity[:response_message] = parse_body(response_env.response_body)
46
+ end
47
+
48
+ def init_log(env)
49
+ {
50
+ method: env.method.to_s.upcase,
51
+ path: env.url.to_s,
52
+ query: query_string(env),
53
+ protocol: "HTTP",
54
+ started_at: Time.now,
55
+ peer: peer(env),
56
+ message: "Faraday HTTP Request"
57
+ }
58
+ end
59
+
60
+ def tracer_info_set(response_env)
61
+ span = OpenTelemetry::Trace.current_span(OpenTelemetry::Context.current)
62
+ context = span.context
63
+ response_env.response_headers[:trace_id] = context.hex_trace_id
64
+ response_env.response_headers[:span_id] = context.hex_span_id
65
+ end
66
+
67
+ def peer(env)
68
+ env.url&.to_s&.match(%r{//([^/]*)/})&.captures&.first || ""
69
+ end
70
+
71
+ def query_string(env)
72
+ env.url&.to_s&.match(/\?.*/).to_s
73
+ end
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ module StrumLogs
4
+ module Helpers
5
+ module HttpLogHelper
6
+ def error_process(log_entity, error, span)
7
+ log_entity[:error] = error.message
8
+ log_entity[:stack_trace] = error.backtrace if Configuration.config.stack_trace
9
+ headers = { "content-type" => "application/json" }
10
+ response = { trace_id: span.context.hex_trace_id, span_id: span.context.hex_span_id, error: error.message }
11
+ span.record_exception(error)
12
+ [500, headers, [Oj.dump(response)]]
13
+ end
14
+
15
+ def logg_process(log_entity, env)
16
+ log_entity[:request] = env["roda.json_params"]
17
+ log_entity[:elapsed_ms] = elapsed_ms(log_entity)
18
+ output(log_entity)
19
+ end
20
+
21
+ def elapsed_ms(log_entity)
22
+ log_entity[:finished] = Time.now
23
+ ((log_entity[:finished] - log_entity[:started_at]) * 1000).round(4)
24
+ end
25
+
26
+ def output(log_entity)
27
+ if log_entity[:error]
28
+ @logger.error(log_entity)
29
+ else
30
+ @logger.info(log_entity)
31
+ end
32
+ end
33
+
34
+ def parse_body(body)
35
+ case body
36
+ when Hash
37
+ body
38
+ when Array
39
+ body.first
40
+ else
41
+ body
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "strum_logs/configuration"
4
+ require "strum_logs/default_logger"
5
+ require "redis"
6
+
7
+ class Redis
8
+ class Client
9
+
10
+ def call(command)
11
+ reply = process([command]) { read }
12
+ StrumLogs::Configuration.config.redis_after_call_hooks.each { |hook| hook.call(command, reply) }
13
+ raise reply if reply.is_a?(CommandError)
14
+
15
+ if block_given? && reply != "QUEUED"
16
+ yield reply
17
+ else
18
+ reply
19
+ end
20
+ end
21
+
22
+ def log(command, reply)
23
+ logger = StrumLogs::DefaultLogger.instance.logger
24
+ logs = { request: command.join(" "), message: "redis request" }
25
+ logger.error(logs.merge({ error: reply })) && return if reply.is_a?(CommandError)
26
+
27
+ logger.info(logs.merge({ response_message: reply }))
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,20 @@
1
+ require "strum_logs/default_logger"
2
+ require 'redis'
3
+
4
+
5
+ module StrumLogs
6
+ module Redis
7
+ class Logger
8
+ def initialize
9
+ @logger = StrumLogs::DefaultLogger.instance.logger
10
+ end
11
+
12
+ def call(command, reply)
13
+ logs = { request: command.join(" "), message: "redis request" }
14
+ @logger.error(logs.merge({ error: reply })) && return if reply.is_a?(::Redis::CommandError)
15
+
16
+ @logger.info(logs.merge({ response_message: reply }))
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,81 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "strum_logs/configuration"
4
+ require "strum_logs/helpers/http_log_helper"
5
+
6
+ module StrumLogs
7
+ module Rack
8
+ class RequestLogMiddleware
9
+ include StrumLogs::Configuration
10
+ include StrumLogs::Helpers::HttpLogHelper
11
+
12
+ def initialize(app)
13
+ @app = app
14
+ @logger = DefaultLogger.instance.logger
15
+ @tracer = DefaultTelemetry.instance.tracer
16
+ end
17
+
18
+ def propagation_context(env)
19
+ getter = OpenTelemetry::Common::Propagation.rack_env_getter
20
+ OpenTelemetry.propagation.extract(env, getter: getter)
21
+ end
22
+
23
+ def call(env)
24
+ OpenTelemetry::Context.with_current(propagation_context(env)) do
25
+ @tracer.in_span(env["PATH_INFO"], kind: :server) do |span|
26
+ logging_process(env, span)
27
+ end
28
+ end
29
+ end
30
+
31
+ def logging_process(env, span)
32
+ log_entity = init_log(env)
33
+ status, headers, body = @app.call(env)
34
+ log_entity[:status] = status
35
+ log_entity[:headers] = headers
36
+ log_entity[:response_message] = parse_body(body)
37
+ log_status(log_entity)
38
+ [status, headers, body]
39
+ rescue StandardError => e
40
+ error_process(log_entity, e, span)
41
+ ensure
42
+ propagate(headers)
43
+ logg_process(log_entity, env)
44
+ end
45
+
46
+ private
47
+
48
+ def propagate(headers)
49
+ return unless StrumLogs::Configuration.config.enable_export_spans
50
+
51
+ setter = OpenTelemetry::Context::Propagation.text_map_setter
52
+ OpenTelemetry.propagation.inject(headers, setter: setter)
53
+ end
54
+
55
+ def log_status(log_entity)
56
+ success?(log_entity[:status]) ? log_entity.merge!({ log_status: "success" }) : log_entity.merge!({ log_status: "error" })
57
+ end
58
+
59
+ def success?(status)
60
+ SUCCESSFUL_STATUSES.include?(status)
61
+ end
62
+
63
+ def init_log(env)
64
+ {
65
+ method: env["REQUEST_METHOD"],
66
+ path: env["PATH_INFO"],
67
+ query: query_string(env),
68
+ protocol: env["SERVER_PROTOCOL"],
69
+ started_at: Time.now,
70
+ peer: env["HTTP_X_FORWARDED_FOR"] || env["REMOTE_ADDR"],
71
+ message: "HTTP Request",
72
+ user: env["REMOTE_USER"]
73
+ }
74
+ end
75
+
76
+ def query_string(env)
77
+ env["QUERY_STRING"] == "" ? nil : env["QUERY_STRING"]
78
+ end
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "sequel"
4
+ require "strum_logs/default_logger"
5
+
6
+ module StrumLogs
7
+ module Sequel
8
+ class Logger
9
+
10
+ def initialize
11
+ @logger = StrumLogs::DefaultLogger.instance.logger
12
+ end
13
+
14
+ def public_send(level, message)
15
+ logs = { message: "sequel request" }
16
+ @logger.error(logs.merge({ error: message })) && return if level == :error
17
+
18
+ @logger.info(logs.merge({ request: message }))
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module StrumLogs
4
+ class LoggingSpanExporter
5
+ def initialize
6
+ @stopped = false
7
+ end
8
+
9
+ def export(spans, _timeout: nil)
10
+ return FAILURE if @stopped
11
+
12
+ Array(spans).each { |s| StrumLogs::DefaultLogger.logger.info(s) }
13
+
14
+ SUCCESS
15
+ end
16
+
17
+ def force_flush(_timeout: nil)
18
+ SUCCESS
19
+ end
20
+
21
+ def shutdown(_timeout: nil)
22
+ @stopped = true
23
+ SUCCESS
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module StrumLogs
4
+ VERSION = "1.0.0"
5
+ end
data/lib/strum_logs.rb ADDED
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "ougai"
4
+ require "opentelemetry/common"
5
+ require "opentelemetry/sdk"
6
+ require "opentelemetry/exporter/otlp"
7
+ require "opentelemetry/instrumentation/pg"
8
+ require "opentelemetry/instrumentation/redis"
9
+ require "opentelemetry/instrumentation/rack"
10
+ require "opentelemetry/instrumentation/faraday"
11
+ require "opentelemetry/instrumentation/sinatra"
12
+ require "strum_logs/request_log_middleware"
13
+ require "strum_logs/default_logger"
14
+ require "strum_logs/configuration"
15
+ require "strum_logs/faraday_log_middleware"
16
+ require "strum_logs/redis_logger"
17
+ require "strum_logs/default_telemetry"
18
+ require "strum_logs/span_exporter"
19
+ require "strum_logs/esb_hooks"
20
+ require "strum_logs/esb_formatter"
21
+ require "strum_logs/errors/configuration_error"
22
+ require "strum_logs/sequel_logger"
23
+ require 'strum_logs/redis_client'
@@ -0,0 +1,6 @@
1
+ module Strum
2
+ module Logs
3
+ VERSION: String
4
+ # See the writing guide of rbs: https://github.com/ruby/rbs#guides
5
+ end
6
+ end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ lib = File.expand_path("lib", __dir__)
4
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
5
+ require "strum_logs/version"
6
+
7
+ Gem::Specification.new do |spec|
8
+ spec.name = "strum-logs"
9
+ spec.version = ::StrumLogs::VERSION
10
+ spec.authors = ["Serhiy Nazarov"]
11
+ spec.email = ["sn@nazarov.com.ua"]
12
+
13
+ spec.summary = "Logs"
14
+ spec.description = "Write a longer description or delete this line."
15
+ spec.require_paths = ["lib"]
16
+ spec.homepage = "https://gitlab.com/strum-rb/strum-logs"
17
+ spec.license = "MIT"
18
+
19
+ spec.required_ruby_version = Gem::Requirement.new(">= 2.7.0")
20
+ spec.metadata = {
21
+ "rubygems_mfa_required" => "true"
22
+ }
23
+ # Specify which files should be added to the gem when it is released.
24
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
25
+ spec.files = Dir.chdir(File.expand_path(__dir__)) do
26
+ `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
27
+ end
28
+ spec.bindir = "exe"
29
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
30
+
31
+ spec.add_runtime_dependency "opentelemetry-common", "~> 0.19"
32
+ spec.add_runtime_dependency "opentelemetry-exporter-otlp", "~> 0.22"
33
+ spec.add_runtime_dependency "opentelemetry-instrumentation-bunny", "~> 0.19"
34
+ spec.add_runtime_dependency "opentelemetry-instrumentation-faraday", "~> 0.21"
35
+ spec.add_runtime_dependency "opentelemetry-instrumentation-pg", "~> 0.21"
36
+ spec.add_runtime_dependency "opentelemetry-instrumentation-rack", "~> 0.21"
37
+ spec.add_runtime_dependency "opentelemetry-instrumentation-redis", "~> 0.23"
38
+ spec.add_runtime_dependency "opentelemetry-instrumentation-sinatra", "~> 0.20"
39
+ spec.add_runtime_dependency "opentelemetry-sdk", "~> 1.1"
40
+ spec.add_runtime_dependency "sequel", "~> 5.24"
41
+
42
+ spec.add_runtime_dependency "amazing_print", "~> 1.4"
43
+ spec.add_runtime_dependency "ougai", "~> 2.0"
44
+ spec.add_development_dependency "faraday", "~> 1.10.0"
45
+ spec.add_development_dependency "faraday_middleware", "~> 1.2.0"
46
+ spec.add_runtime_dependency "rack", "~> 2.2.4"
47
+ spec.add_development_dependency "rake", "~> 13"
48
+ spec.add_runtime_dependency "redis"
49
+ spec.add_development_dependency "rubocop", "~> 1.30"
50
+ spec.add_development_dependency "strum-esb"
51
+ spec.add_runtime_dependency "dry-configurable", "~> 0.12.1"
52
+ end