strum-logs 1.0.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.
@@ -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