canonical_log 0.1.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.
- checksums.yaml +7 -0
- data/LICENSE.txt +21 -0
- data/lib/canonical_log/configuration.rb +42 -0
- data/lib/canonical_log/context.rb +19 -0
- data/lib/canonical_log/event.rb +79 -0
- data/lib/canonical_log/integrations/error_enrichment.rb +27 -0
- data/lib/canonical_log/integrations/sidekiq.rb +47 -0
- data/lib/canonical_log/middleware.rb +100 -0
- data/lib/canonical_log/railtie.rb +14 -0
- data/lib/canonical_log/sampling.rb +21 -0
- data/lib/canonical_log/sinks/base.rb +13 -0
- data/lib/canonical_log/sinks/rails_logger.rb +11 -0
- data/lib/canonical_log/sinks/stdout.rb +11 -0
- data/lib/canonical_log/subscribers/action_controller.rb +46 -0
- data/lib/canonical_log/subscribers/active_record.rb +35 -0
- data/lib/canonical_log/version.rb +5 -0
- data/lib/canonical_log.rb +60 -0
- data/lib/generators/canonical_log/install_generator.rb +17 -0
- data/lib/generators/canonical_log/templates/canonical_log.rb +35 -0
- metadata +89 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 9fa4e04f9113d21ee261935a7640d755f2e214bbb270b5b13a0edaa701c79afc
|
|
4
|
+
data.tar.gz: d6ba73178b27401e019ab2b884dd291e870e041fd3a79cf6362913b7c3812497
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: b020eafa421cda0eae82281f78a4eb2a58818ab2bb1f2455cc61e806312daf63e83967a44a7ec7716514988a88e3f06ef525574e63bc53edbae1b3c8deb6e19f
|
|
7
|
+
data.tar.gz: 5813f35498dba878fb721cdfc83e4f71cbef68f977d4e43bf8965040b10577188477b14f280f92195908b727f5e0a9fe65f3e239e9a6d03597e9a5018c9520b0
|
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Krzysztof Duda
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
|
13
|
+
all copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
21
|
+
THE SOFTWARE.
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module CanonicalLog
|
|
4
|
+
class Configuration
|
|
5
|
+
attr_accessor :sinks, :param_filter_keys, :slow_query_threshold_ms,
|
|
6
|
+
:user_context, :before_emit, :ignored_paths,
|
|
7
|
+
:sample_rate, :slow_request_threshold_ms, :sampling
|
|
8
|
+
|
|
9
|
+
def initialize
|
|
10
|
+
@sinks = :auto
|
|
11
|
+
@param_filter_keys = %w[password password_confirmation token secret]
|
|
12
|
+
@slow_query_threshold_ms = 100.0
|
|
13
|
+
@user_context = nil
|
|
14
|
+
@before_emit = nil
|
|
15
|
+
@ignored_paths = []
|
|
16
|
+
@sample_rate = 1.0 # Log everything by default
|
|
17
|
+
@slow_request_threshold_ms = 2000.0
|
|
18
|
+
@sampling = nil # Custom sampling proc, receives (event_hash, config) -> bool
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def resolved_sinks
|
|
22
|
+
case @sinks
|
|
23
|
+
when :auto
|
|
24
|
+
[CanonicalLog::Sinks::Stdout.new]
|
|
25
|
+
when Array
|
|
26
|
+
@sinks
|
|
27
|
+
else
|
|
28
|
+
[@sinks]
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def should_sample?(event_hash)
|
|
33
|
+
if @sampling
|
|
34
|
+
@sampling.call(event_hash, self)
|
|
35
|
+
elsif @sample_rate >= 1.0
|
|
36
|
+
true
|
|
37
|
+
else
|
|
38
|
+
Sampling.default(event_hash, self)
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module CanonicalLog
|
|
4
|
+
module Context
|
|
5
|
+
THREAD_KEY = :canonical_log_event
|
|
6
|
+
|
|
7
|
+
def self.init!
|
|
8
|
+
Thread.current[THREAD_KEY] = Event.new
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def self.current
|
|
12
|
+
Thread.current[THREAD_KEY]
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def self.clear!
|
|
16
|
+
Thread.current[THREAD_KEY] = nil
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'json'
|
|
4
|
+
|
|
5
|
+
module CanonicalLog
|
|
6
|
+
class Event
|
|
7
|
+
CATEGORIES = %i[user business infra service].freeze
|
|
8
|
+
|
|
9
|
+
def initialize
|
|
10
|
+
@fields = {}
|
|
11
|
+
@categories = {}
|
|
12
|
+
@mutex = Mutex.new
|
|
13
|
+
@started_at = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def add(hash)
|
|
17
|
+
@mutex.synchronize do
|
|
18
|
+
@fields.merge!(hash)
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def set(key, value)
|
|
23
|
+
@mutex.synchronize do
|
|
24
|
+
@fields[key.to_sym] = value
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def increment(key, by = 1)
|
|
29
|
+
@mutex.synchronize do
|
|
30
|
+
@fields[key.to_sym] = (@fields[key.to_sym] || 0) + by
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def append(key, value)
|
|
35
|
+
@mutex.synchronize do
|
|
36
|
+
@fields[key.to_sym] ||= []
|
|
37
|
+
@fields[key.to_sym] << value
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Categorized context: event.context(:user, id: 123, tier: "premium")
|
|
42
|
+
def context(category, data)
|
|
43
|
+
raise ArgumentError, "Unknown category: #{category}" unless CATEGORIES.include?(category.to_sym)
|
|
44
|
+
|
|
45
|
+
@mutex.synchronize do
|
|
46
|
+
@categories[category.to_sym] ||= {}
|
|
47
|
+
@categories[category.to_sym].merge!(data)
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Structured error capture
|
|
52
|
+
def add_error(error, metadata = {})
|
|
53
|
+
@mutex.synchronize do
|
|
54
|
+
@fields[:error] = {
|
|
55
|
+
class: error.class.name,
|
|
56
|
+
message: error.message
|
|
57
|
+
}.merge(metadata)
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def duration_ms
|
|
62
|
+
elapsed = Process.clock_gettime(Process::CLOCK_MONOTONIC) - @started_at
|
|
63
|
+
(elapsed * 1000).round(2)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def to_h
|
|
67
|
+
@mutex.synchronize do
|
|
68
|
+
result = { timestamp: Time.now.utc.iso8601(3), duration_ms: duration_ms }
|
|
69
|
+
result.merge!(@fields)
|
|
70
|
+
@categories.each { |cat, data| result[cat] = data unless data.empty? }
|
|
71
|
+
result
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def to_json(*_args)
|
|
76
|
+
to_h.to_json
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'active_support/concern'
|
|
4
|
+
|
|
5
|
+
module CanonicalLog
|
|
6
|
+
module Integrations
|
|
7
|
+
module ErrorEnrichment
|
|
8
|
+
extend ActiveSupport::Concern
|
|
9
|
+
|
|
10
|
+
included do
|
|
11
|
+
around_action :capture_errors_for_canonical_log
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
private
|
|
15
|
+
|
|
16
|
+
def capture_errors_for_canonical_log
|
|
17
|
+
yield
|
|
18
|
+
rescue StandardError => e
|
|
19
|
+
CanonicalLog.add(
|
|
20
|
+
rescued_error_class: e.class.name,
|
|
21
|
+
rescued_error_message: e.message
|
|
22
|
+
)
|
|
23
|
+
raise
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module CanonicalLog
|
|
4
|
+
module Integrations
|
|
5
|
+
class Sidekiq
|
|
6
|
+
def call(_job_instance, msg, queue)
|
|
7
|
+
Context.init!
|
|
8
|
+
event = Context.current
|
|
9
|
+
event.add(
|
|
10
|
+
job_class: msg['class'],
|
|
11
|
+
queue: queue,
|
|
12
|
+
jid: msg['jid']
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
yield
|
|
16
|
+
rescue Exception => e # rubocop:disable Lint/RescueException
|
|
17
|
+
event&.add(
|
|
18
|
+
error_class: e.class.name,
|
|
19
|
+
error_message: e.message
|
|
20
|
+
)
|
|
21
|
+
raise
|
|
22
|
+
ensure
|
|
23
|
+
emit!
|
|
24
|
+
Context.clear!
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
private
|
|
28
|
+
|
|
29
|
+
def emit!
|
|
30
|
+
event = Context.current
|
|
31
|
+
return unless event
|
|
32
|
+
|
|
33
|
+
config = CanonicalLog.configuration
|
|
34
|
+
config.before_emit&.call(event)
|
|
35
|
+
json = event.to_json
|
|
36
|
+
|
|
37
|
+
config.resolved_sinks.each do |sink|
|
|
38
|
+
sink.write(json)
|
|
39
|
+
rescue StandardError => e
|
|
40
|
+
warn "[CanonicalLog] Sink error (#{sink.class}): #{e.message}"
|
|
41
|
+
end
|
|
42
|
+
rescue StandardError => e
|
|
43
|
+
warn "[CanonicalLog] Emit error: #{e.message}"
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module CanonicalLog
|
|
4
|
+
class Middleware
|
|
5
|
+
def initialize(app)
|
|
6
|
+
@app = app
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def call(env)
|
|
10
|
+
return @app.call(env) if ignored_path?(env)
|
|
11
|
+
|
|
12
|
+
Context.init!
|
|
13
|
+
seed_request_fields(env)
|
|
14
|
+
|
|
15
|
+
status, headers, body = @app.call(env)
|
|
16
|
+
Context.current&.set(:http_status, status)
|
|
17
|
+
enrich_user_context(env)
|
|
18
|
+
[status, headers, body]
|
|
19
|
+
rescue Exception => e # rubocop:disable Lint/RescueException
|
|
20
|
+
Context.current&.add_error(e)
|
|
21
|
+
Context.current&.set(:http_status, 500)
|
|
22
|
+
raise
|
|
23
|
+
ensure
|
|
24
|
+
emit! if Context.current
|
|
25
|
+
Context.clear!
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
private
|
|
29
|
+
|
|
30
|
+
def seed_request_fields(env)
|
|
31
|
+
event = Context.current
|
|
32
|
+
return unless event
|
|
33
|
+
|
|
34
|
+
request_id = env['action_dispatch.request_id'] ||
|
|
35
|
+
env['HTTP_X_REQUEST_ID'] ||
|
|
36
|
+
SecureRandom.uuid
|
|
37
|
+
|
|
38
|
+
event.add(
|
|
39
|
+
request_id: request_id,
|
|
40
|
+
http_method: env['REQUEST_METHOD'],
|
|
41
|
+
path: env['PATH_INFO'],
|
|
42
|
+
query_string: env['QUERY_STRING'].to_s.empty? ? nil : env['QUERY_STRING'],
|
|
43
|
+
remote_ip: env['HTTP_X_FORWARDED_FOR'] || env['REMOTE_ADDR'],
|
|
44
|
+
user_agent: env['HTTP_USER_AGENT'],
|
|
45
|
+
content_type: env['CONTENT_TYPE']
|
|
46
|
+
)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def enrich_user_context(env)
|
|
50
|
+
event = Context.current
|
|
51
|
+
return unless event
|
|
52
|
+
|
|
53
|
+
config = CanonicalLog.configuration
|
|
54
|
+
if config.user_context
|
|
55
|
+
begin
|
|
56
|
+
user_fields = config.user_context.call(env)
|
|
57
|
+
event.add(user_fields) if user_fields.is_a?(Hash)
|
|
58
|
+
rescue StandardError => e
|
|
59
|
+
warn "[CanonicalLog] user_context error: #{e.message}"
|
|
60
|
+
end
|
|
61
|
+
elsif defined?(Warden::Manager) && env['warden']
|
|
62
|
+
user = env['warden'].user rescue nil
|
|
63
|
+
if user
|
|
64
|
+
event.context(:user, id: user.try(:id), email: user.try(:email))
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def emit!
|
|
70
|
+
event = Context.current
|
|
71
|
+
return unless event
|
|
72
|
+
|
|
73
|
+
config = CanonicalLog.configuration
|
|
74
|
+
config.before_emit&.call(event)
|
|
75
|
+
|
|
76
|
+
event_hash = event.to_h
|
|
77
|
+
return unless config.should_sample?(event_hash)
|
|
78
|
+
|
|
79
|
+
json = event_hash.to_json
|
|
80
|
+
|
|
81
|
+
config.resolved_sinks.each do |sink|
|
|
82
|
+
sink.write(json)
|
|
83
|
+
rescue StandardError => e
|
|
84
|
+
warn "[CanonicalLog] Sink error (#{sink.class}): #{e.message}"
|
|
85
|
+
end
|
|
86
|
+
rescue StandardError => e
|
|
87
|
+
warn "[CanonicalLog] Emit error: #{e.message}"
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def ignored_path?(env)
|
|
91
|
+
path = env['PATH_INFO']
|
|
92
|
+
CanonicalLog.configuration.ignored_paths.any? do |pattern|
|
|
93
|
+
case pattern
|
|
94
|
+
when Regexp then pattern.match?(path)
|
|
95
|
+
when String then path.start_with?(pattern)
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module CanonicalLog
|
|
4
|
+
class Railtie < Rails::Railtie
|
|
5
|
+
initializer 'canonical_log.insert_middleware' do |app|
|
|
6
|
+
app.middleware.insert(0, CanonicalLog::Middleware)
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
initializer 'canonical_log.subscribe' do
|
|
10
|
+
CanonicalLog::Subscribers::ActionController.subscribe!
|
|
11
|
+
CanonicalLog::Subscribers::ActiveRecord.subscribe!
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module CanonicalLog
|
|
4
|
+
module Sampling
|
|
5
|
+
# Default sampling: always keep errors and slow requests, sample the rest.
|
|
6
|
+
def self.default(event_hash, config)
|
|
7
|
+
status = event_hash[:http_status] || 0
|
|
8
|
+
duration = event_hash[:duration_ms] || 0
|
|
9
|
+
|
|
10
|
+
# Always keep errors
|
|
11
|
+
return true if status >= 500
|
|
12
|
+
return true if event_hash[:error]
|
|
13
|
+
|
|
14
|
+
# Always keep slow requests
|
|
15
|
+
return true if duration >= config.slow_request_threshold_ms
|
|
16
|
+
|
|
17
|
+
# Sample the rest
|
|
18
|
+
rand < config.sample_rate
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module CanonicalLog
|
|
4
|
+
module Sinks
|
|
5
|
+
# Duck-type interface for sinks.
|
|
6
|
+
# Any object responding to #write(json_string) can be used as a sink.
|
|
7
|
+
class Base
|
|
8
|
+
def write(json_string)
|
|
9
|
+
raise NotImplementedError, "#{self.class} must implement #write"
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module CanonicalLog
|
|
4
|
+
module Subscribers
|
|
5
|
+
module ActionController
|
|
6
|
+
def self.subscribe!
|
|
7
|
+
ActiveSupport::Notifications.subscribe('process_action.action_controller') do |*args|
|
|
8
|
+
event = ActiveSupport::Notifications::Event.new(*args)
|
|
9
|
+
handle(event)
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def self.handle(notification)
|
|
14
|
+
event = Context.current
|
|
15
|
+
return unless event
|
|
16
|
+
|
|
17
|
+
payload = notification.payload
|
|
18
|
+
config = CanonicalLog.configuration
|
|
19
|
+
|
|
20
|
+
params = (payload[:params] || {}).except('controller', 'action')
|
|
21
|
+
filtered_params = filter_params(params, config.param_filter_keys)
|
|
22
|
+
|
|
23
|
+
event.add(
|
|
24
|
+
controller: payload[:controller],
|
|
25
|
+
action: payload[:action],
|
|
26
|
+
format: payload[:format],
|
|
27
|
+
params: filtered_params,
|
|
28
|
+
view_runtime_ms: payload[:view_runtime]&.round(2),
|
|
29
|
+
db_runtime_ms: payload[:db_runtime]&.round(2)
|
|
30
|
+
)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def self.filter_params(params, filter_keys)
|
|
34
|
+
params.each_with_object({}) do |(key, value), filtered|
|
|
35
|
+
filtered[key] = if filter_keys.include?(key.to_s)
|
|
36
|
+
'[FILTERED]'
|
|
37
|
+
elsif value.is_a?(Hash)
|
|
38
|
+
filter_params(value, filter_keys)
|
|
39
|
+
else
|
|
40
|
+
value
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module CanonicalLog
|
|
4
|
+
module Subscribers
|
|
5
|
+
module ActiveRecord
|
|
6
|
+
def self.subscribe!
|
|
7
|
+
ActiveSupport::Notifications.subscribe('sql.active_record') do |*args|
|
|
8
|
+
event = ActiveSupport::Notifications::Event.new(*args)
|
|
9
|
+
handle(event)
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def self.handle(notification)
|
|
14
|
+
event = Context.current
|
|
15
|
+
return unless event
|
|
16
|
+
|
|
17
|
+
payload = notification.payload
|
|
18
|
+
return if %w[SCHEMA CACHE].include?(payload[:name])
|
|
19
|
+
|
|
20
|
+
duration_ms = notification.duration
|
|
21
|
+
event.increment(:db_query_count)
|
|
22
|
+
event.increment(:db_total_time_ms, duration_ms.round(2))
|
|
23
|
+
|
|
24
|
+
threshold = CanonicalLog.configuration.slow_query_threshold_ms
|
|
25
|
+
return unless duration_ms >= threshold
|
|
26
|
+
|
|
27
|
+
event.append(:slow_queries, {
|
|
28
|
+
sql: payload[:sql]&.slice(0, 500),
|
|
29
|
+
duration_ms: duration_ms.round(2),
|
|
30
|
+
name: payload[:name]
|
|
31
|
+
})
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'active_support'
|
|
4
|
+
|
|
5
|
+
require_relative 'canonical_log/version'
|
|
6
|
+
require_relative 'canonical_log/configuration'
|
|
7
|
+
require_relative 'canonical_log/sampling'
|
|
8
|
+
require_relative 'canonical_log/event'
|
|
9
|
+
require_relative 'canonical_log/context'
|
|
10
|
+
require_relative 'canonical_log/middleware'
|
|
11
|
+
require_relative 'canonical_log/sinks/base'
|
|
12
|
+
require_relative 'canonical_log/sinks/stdout'
|
|
13
|
+
require_relative 'canonical_log/sinks/rails_logger'
|
|
14
|
+
require_relative 'canonical_log/subscribers/action_controller'
|
|
15
|
+
require_relative 'canonical_log/subscribers/active_record'
|
|
16
|
+
|
|
17
|
+
require_relative 'canonical_log/railtie' if defined?(Rails::Railtie)
|
|
18
|
+
|
|
19
|
+
module CanonicalLog
|
|
20
|
+
class << self
|
|
21
|
+
def configuration
|
|
22
|
+
@configuration ||= Configuration.new
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def configure
|
|
26
|
+
yield(configuration)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def reset_configuration!
|
|
30
|
+
@configuration = Configuration.new
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Convenience delegations to current event
|
|
34
|
+
def add(hash)
|
|
35
|
+
Context.current&.add(hash)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def set(key, value)
|
|
39
|
+
Context.current&.set(key, value)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def increment(key, by = 1)
|
|
43
|
+
Context.current&.increment(key, by)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def append(key, value)
|
|
47
|
+
Context.current&.append(key, value)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Categorized context: CanonicalLog.context(:user, id: 123)
|
|
51
|
+
def context(category, data)
|
|
52
|
+
Context.current&.context(category, data)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Structured error: CanonicalLog.add_error(exception)
|
|
56
|
+
def add_error(error, metadata = {})
|
|
57
|
+
Context.current&.add_error(error, metadata)
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'rails/generators'
|
|
4
|
+
|
|
5
|
+
module CanonicalLog
|
|
6
|
+
module Generators
|
|
7
|
+
class InstallGenerator < Rails::Generators::Base
|
|
8
|
+
source_root File.expand_path('templates', __dir__)
|
|
9
|
+
|
|
10
|
+
desc 'Creates a CanonicalLog initializer in config/initializers'
|
|
11
|
+
|
|
12
|
+
def copy_initializer
|
|
13
|
+
template 'canonical_log.rb', 'config/initializers/canonical_log.rb'
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
CanonicalLog.configure do |config|
|
|
4
|
+
# Sinks determine where the canonical log line is written.
|
|
5
|
+
# :auto uses RailsLogger in development, Stdout in production.
|
|
6
|
+
# config.sinks = :auto
|
|
7
|
+
# config.sinks = [CanonicalLog::Sinks::Stdout.new]
|
|
8
|
+
# config.sinks = [CanonicalLog::Sinks::RailsLogger.new]
|
|
9
|
+
|
|
10
|
+
# Parameter keys to filter from log output (replaced with [FILTERED]).
|
|
11
|
+
# config.param_filter_keys = %w[password password_confirmation token secret]
|
|
12
|
+
|
|
13
|
+
# SQL queries slower than this threshold (in ms) are captured individually.
|
|
14
|
+
# config.slow_query_threshold_ms = 100.0
|
|
15
|
+
|
|
16
|
+
# Proc to extract user context from the controller notification.
|
|
17
|
+
# Receives an ActiveSupport::Notifications::Event and should return a Hash.
|
|
18
|
+
# config.user_context = ->(notification) {
|
|
19
|
+
# controller = notification.payload[:headers]&.env&.dig("action_controller.instance")
|
|
20
|
+
# if controller&.respond_to?(:current_user) && controller.current_user
|
|
21
|
+
# { user_id: controller.current_user.id }
|
|
22
|
+
# else
|
|
23
|
+
# {}
|
|
24
|
+
# end
|
|
25
|
+
# }
|
|
26
|
+
|
|
27
|
+
# Hook called with the Event just before it is serialized and emitted.
|
|
28
|
+
# config.before_emit = ->(event) {
|
|
29
|
+
# event.set(:app_version, ENV["APP_VERSION"])
|
|
30
|
+
# }
|
|
31
|
+
|
|
32
|
+
# Paths to ignore (no canonical log line will be emitted).
|
|
33
|
+
# Supports strings (prefix match) and regexps.
|
|
34
|
+
# config.ignored_paths = ["/health", "/assets", %r{\A/packs}]
|
|
35
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: canonical_log
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Krzysztof Duda
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: activesupport
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - ">="
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '6.0'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - ">="
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '6.0'
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: rack
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - ">="
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '2.0'
|
|
33
|
+
type: :runtime
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - ">="
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '2.0'
|
|
40
|
+
description: Implements the canonical log lines / wide events pattern. Accumulates
|
|
41
|
+
context throughout a request lifecycle and emits a single structured JSON log line
|
|
42
|
+
containing everything interesting.
|
|
43
|
+
email:
|
|
44
|
+
- hello@krzysztof.studio
|
|
45
|
+
executables: []
|
|
46
|
+
extensions: []
|
|
47
|
+
extra_rdoc_files: []
|
|
48
|
+
files:
|
|
49
|
+
- LICENSE.txt
|
|
50
|
+
- lib/canonical_log.rb
|
|
51
|
+
- lib/canonical_log/configuration.rb
|
|
52
|
+
- lib/canonical_log/context.rb
|
|
53
|
+
- lib/canonical_log/event.rb
|
|
54
|
+
- lib/canonical_log/integrations/error_enrichment.rb
|
|
55
|
+
- lib/canonical_log/integrations/sidekiq.rb
|
|
56
|
+
- lib/canonical_log/middleware.rb
|
|
57
|
+
- lib/canonical_log/railtie.rb
|
|
58
|
+
- lib/canonical_log/sampling.rb
|
|
59
|
+
- lib/canonical_log/sinks/base.rb
|
|
60
|
+
- lib/canonical_log/sinks/rails_logger.rb
|
|
61
|
+
- lib/canonical_log/sinks/stdout.rb
|
|
62
|
+
- lib/canonical_log/subscribers/action_controller.rb
|
|
63
|
+
- lib/canonical_log/subscribers/active_record.rb
|
|
64
|
+
- lib/canonical_log/version.rb
|
|
65
|
+
- lib/generators/canonical_log/install_generator.rb
|
|
66
|
+
- lib/generators/canonical_log/templates/canonical_log.rb
|
|
67
|
+
homepage: https://github.com/krzysztoff1/canonical_log
|
|
68
|
+
licenses:
|
|
69
|
+
- MIT
|
|
70
|
+
metadata:
|
|
71
|
+
rubygems_mfa_required: 'true'
|
|
72
|
+
rdoc_options: []
|
|
73
|
+
require_paths:
|
|
74
|
+
- lib
|
|
75
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
76
|
+
requirements:
|
|
77
|
+
- - ">="
|
|
78
|
+
- !ruby/object:Gem::Version
|
|
79
|
+
version: 3.0.0
|
|
80
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
81
|
+
requirements:
|
|
82
|
+
- - ">="
|
|
83
|
+
- !ruby/object:Gem::Version
|
|
84
|
+
version: '0'
|
|
85
|
+
requirements: []
|
|
86
|
+
rubygems_version: 3.6.7
|
|
87
|
+
specification_version: 4
|
|
88
|
+
summary: One structured JSON log line per request
|
|
89
|
+
test_files: []
|