activesupport-json_logging 1.2.2 → 1.3.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 71ac52d26c4cca14a0eaed618927f9432880865ad9b24e43232b5801ce10a055
4
- data.tar.gz: 81ef2c90e5d44fc7910f319acdbbd4bb9b56572075c6a28050fd05759349e91b
3
+ metadata.gz: 2b40f43e3cd600bfc139665fa3e6cc9df7a4a56eab58bec7f760b986faa9d2f7
4
+ data.tar.gz: d8d3352e482dc9698aa94a499f9905a1fdec0ca157cedf4e8a61194dfd7e7fba
5
5
  SHA512:
6
- metadata.gz: 0f8ebdfeb94e456cdffd8e8d176c9dde7dff4187f2547b721dd4ec33e229c290663386a1f1c091ee54e95821be3bc11eb8f02fbf645f3a9bd8a54ab1e79596b9
7
- data.tar.gz: cbfdad0f75642ec096bf18ca154a9aef9ce3364d92a43c263ceea81f877f018db3459b1a3fc239b2025aa6d96815db8f37063675f33a29d24fd24de4eaeb2299
6
+ metadata.gz: 4b0befde4733aeade6f332adaf529735ed718fc5b4ad03dbbcbcd6d66e0124ed107e508f47fc3efa70f4fc31479d61ac42508d364c185c846a9c93ce51924cac
7
+ data.tar.gz: 83b705a450abaf4b389495652843bc4ed537de7485bb1158423b5a9055e40616024ed928844a485d65aa26cadfbe64746570dd823943dc724b29e849dcbd7ecf
data/CHANGELOG.md CHANGED
@@ -1,5 +1,10 @@
1
1
  # CHANGELOG
2
2
 
3
+ ## 1.3.0 (2026-07-25)
4
+
5
+ - BREAKING: Require Ruby 3.2+ and Rails 7+ (drop Ruby 2.7–3.1 and Rails 6 support)
6
+ - Add opt-in `JsonLogging::EventSubscriber` for Rails 8.1 `ActiveSupport::EventReporter` / `Rails.event` structured events
7
+
3
8
  ## 1.2.3 (2026-07-13)
4
9
 
5
10
  - Reduce per-line work for plain string, hash, tagged, and context-scoped log messages on common shapes
data/README.md CHANGED
@@ -1,17 +1,10 @@
1
1
  # activesupport-json_logging
2
2
 
3
- [![Gem Version](https://badge.fury.io/rb/activesupport-json_logging.svg)](https://badge.fury.io/rb/activesupport-json_logging) [![Test Status](https://github.com/amkisko/activesupport-json_logging.rb/actions/workflows/test.yml/badge.svg)](https://github.com/amkisko/activesupport-json_logging.rb/actions/workflows/test.yml) [![codecov](https://codecov.io/gh/amkisko/activesupport-json_logging.rb/graph/badge.svg?token=UX80FTO0Y0)](https://codecov.io/gh/amkisko/activesupport-json_logging.rb) [![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=amkisko_activesupport-json_logging.rb&metric=alert_status)](https://sonarcloud.io/summary/new_code?id=amkisko_activesupport-json_logging.rb)
3
+ [![Gem Version](https://badge.fury.io/rb/activesupport-json_logging.svg)](https://badge.fury.io/rb/activesupport-json_logging) [![Test Status](https://github.com/amkisko/activesupport-json_logging.rb/actions/workflows/test.yml/badge.svg)](https://github.com/amkisko/activesupport-json_logging.rb/actions/workflows/test.yml) [![codecov](https://codecov.io/gh/amkisko/activesupport-json_logging.rb/graph/badge.svg?token=UX80FTO0Y0)](https://app.codecov.io/github/amkisko/activesupport-json_logging.rb) [![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=amkisko_activesupport-json_logging.rb&metric=alert_status)](https://sonarcloud.io/project/overview?id=amkisko_activesupport-json_logging.rb)
4
4
 
5
5
  Structured JSON logging for Rails and ActiveSupport with a safe, single-line formatter.
6
6
  No dependencies beyond Rails and Activesupport.
7
- Supports Rails versions from 6 to 8.
8
-
9
- Sponsored by [Kisko Labs](https://www.kiskolabs.com).
10
-
11
- <a href="https://www.kiskolabs.com">
12
- <img src="kisko.svg" width="200" alt="Sponsored by Kisko Labs" />
13
- </a>
14
-
7
+ Supports Ruby 3.2+ and Rails 7–8.
15
8
 
16
9
  ## Installation
17
10
 
@@ -46,6 +39,7 @@ end
46
39
  - Native `tagged` method support - use it just like Rails' tagged logger
47
40
  - Service-specific tagged loggers - create loggers with permanent tags using `logger.tagged("service")` without a block
48
41
  - Full compatibility with `ActiveSupport::BroadcastLogger` (Rails 7.1+)
42
+ - Opt-in `JsonLogging::EventSubscriber` for Rails 8.1 `ActiveSupport::EventReporter` / `Rails.event`
49
43
  - Automatic Rails integration via Railtie (auto-requires the gem in Rails apps)
50
44
 
51
45
  ## Basic usage
@@ -132,6 +126,37 @@ sidekiq_logger.info("Job enqueued") # Tagged with "sidekiq"
132
126
  api_logger.info("Request received") # Tagged with "api"
133
127
  ```
134
128
 
129
+ ### Rails 8.1 Event Reporter
130
+
131
+ Rails 8.1 adds `Rails.event` (`ActiveSupport::EventReporter`) for structured events that complement `Rails.logger`. Setting `config.logger` to a `JsonLogging` logger does not subscribe you to events. Register a subscriber:
132
+
133
+ ```ruby
134
+ # config/initializers/json_logging.rb
135
+ subscriber = JsonLogging::EventSubscriber.new(logger: -> { Rails.logger })
136
+ Rails.event.subscribe(subscriber)
137
+
138
+ # Optional: only a subset of events
139
+ Rails.event.subscribe(subscriber) { |event| event[:name].start_with?("app.") }
140
+ ```
141
+
142
+ Pass either `logger:` or `io:`, not both. A callable `logger:` is resolved on each emit (the Railtie opt-in uses this so a later `Rails.logger=` swap is picked up).
143
+
144
+ Or enable the Railtie opt-in (off by default):
145
+
146
+ ```ruby
147
+ # config/application.rb or an environment file
148
+ config.json_logging.subscribe_event_reporter = true
149
+ ```
150
+
151
+ The subscriber writes one JSON line per event and keeps the Rails event shape (`name`, `payload`, `tags`, `context`, `timestamp`, `source_location`). It writes through `Logger#<<` so the logger formatter does not wrap the event again. Payload and tag objects that implement `#serialize` are encoded via that method. Encode and write failures never raise from `#emit`.
152
+
153
+ ```ruby
154
+ Rails.event.set_context(request_id: "abc123")
155
+ Rails.event.tagged("billing") do
156
+ Rails.event.notify("invoice.created", invoice_id: 3)
157
+ end
158
+ ```
159
+
135
160
  ### BroadcastLogger integration
136
161
 
137
162
  `ActiveSupport::BroadcastLogger` (Rails 7.1+) allows writing logs to multiple destinations simultaneously. `JsonLogging` works seamlessly with `BroadcastLogger`:
@@ -460,6 +485,16 @@ formatter.call("INFO", Time.now, nil, "message") # Output includes both tags
460
485
 
461
486
  **Note:** When used with a logger (via `JsonLogging.new`), the logger uses `FormatterWithTags` which automatically includes tags from the logger's tagged context. Use `Formatter` directly only when you need a standalone formatter without a logger instance.
462
487
 
488
+ ### JsonLogging::EventSubscriber
489
+
490
+ Rails 8.1+ subscriber for `ActiveSupport::EventReporter`. See [Rails 8.1 Event Reporter](#rails-81-event-reporter).
491
+
492
+ ```ruby
493
+ subscriber = JsonLogging::EventSubscriber.new(logger: -> { Rails.logger })
494
+ # or: JsonLogging::EventSubscriber.new(io: $stdout)
495
+ Rails.event.subscribe(subscriber)
496
+ ```
497
+
463
498
  ### JsonLogging.with_context
464
499
 
465
500
  Add thread-local context that appears in all log entries within the block:
@@ -554,7 +589,7 @@ This is useful for:
554
589
  - Temporary verbose logging in background jobs
555
590
  - Per-request log level changes
556
591
 
557
- **Note:** `local_level` is available in Rails 7.1+. In Rails 6-7.0, only the global `level` is available.
592
+ **Note:** `local_level` is available in Rails 7.1+. In Rails 7.0, only the global `level` is available.
558
593
 
559
594
  #### Standard Logger Methods
560
595
 
@@ -652,7 +687,28 @@ gem build activesupport-json_logging.gemspec
652
687
  gem push activesupport-json_logging-*.gem
653
688
  ```
654
689
 
690
+ ## Links
691
+
692
+ - [GitHub](https://github.com/amkisko/activesupport-json_logging.rb)
693
+ - [GitLab](https://gitlab.com/amkisko/activesupport-json_logging.rb)
694
+ - [RubyGems](https://rubygems.org/gems/activesupport-json_logging)
695
+ - [Versions Atom](https://rubygems.org/gems/activesupport-json_logging/versions.atom) (feed id `9801944970071`)
696
+ - [libraries.io](https://libraries.io/rubygems/activesupport-json_logging)
697
+ - [Deps.dev](https://deps.dev/rubygems/activesupport-json_logging)
698
+ - [SonarCloud](https://sonarcloud.io/project/overview?id=amkisko_activesupport-json_logging.rb)
699
+ - [Snyk](https://snyk.io/test/github/amkisko/activesupport-json_logging.rb)
700
+ - [Codecov](https://app.codecov.io/github/amkisko/activesupport-json_logging.rb)
701
+ - [OpenSSF Scorecard](https://scorecard.dev/viewer/?uri=github.com/amkisko/activesupport-json_logging.rb)
702
+
655
703
  ## License
656
704
 
657
705
  The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
658
706
 
707
+ ## Sponsors
708
+
709
+ Sponsored by [Kisko Labs](https://www.kiskolabs.com).
710
+
711
+ <a href="https://www.kiskolabs.com">
712
+ <img src="kisko.svg" width="200" alt="Sponsored by Kisko Labs" />
713
+ </a>
714
+
@@ -2,7 +2,18 @@ require "json_logging"
2
2
 
3
3
  module JsonLogging
4
4
  class Railtie < ::Rails::Railtie
5
+ config.json_logging = ActiveSupport::OrderedOptions.new
6
+ config.json_logging.subscribe_event_reporter = false
7
+
5
8
  # This Railtie ensures json_logging is automatically required when Rails loads
6
9
  # Without this, users would need to manually require "json_logging" in initializers
10
+
11
+ initializer "json_logging.subscribe_event_reporter", after: :load_config_initializers do |app|
12
+ next unless app.config.json_logging.subscribe_event_reporter
13
+ next unless defined?(ActiveSupport::EventReporter)
14
+ next unless Rails.respond_to?(:event)
15
+
16
+ Rails.event.subscribe(JsonLogging::EventSubscriber.new(logger: -> { Rails.logger }))
17
+ end
7
18
  end
8
19
  end
@@ -0,0 +1,146 @@
1
+ module JsonLogging
2
+ # Opt-in subscriber for ActiveSupport::EventReporter (Rails 8.1+).
3
+ # Preserves the Rails event hash shape as a single JSON line.
4
+ class EventSubscriber
5
+ def initialize(logger: nil, io: nil)
6
+ if !logger.nil? && !io.nil?
7
+ raise ArgumentError, "JsonLogging::EventSubscriber accepts only one of logger: or io:"
8
+ end
9
+ if logger.nil? && io.nil?
10
+ raise ArgumentError, "JsonLogging::EventSubscriber requires logger: or io:"
11
+ end
12
+
13
+ @logger = logger
14
+ @io = io
15
+ end
16
+
17
+ def emit(event)
18
+ line = begin
19
+ encode_event(event)
20
+ rescue => error
21
+ encode_fallback(event, error)
22
+ end
23
+
24
+ write_line(line)
25
+ rescue => error
26
+ report_write_error(error)
27
+ nil
28
+ end
29
+
30
+ private
31
+
32
+ def encode_event(event)
33
+ payload = {
34
+ "name" => Helpers.safe_string(event_field(event, :name)),
35
+ "payload" => serialize_payload(event_field(event, :payload)),
36
+ "tags" => serialize_tags(event_field(event, :tags)),
37
+ "context" => hash_or_empty(event_field(event, :context)),
38
+ "timestamp" => event_field(event, :timestamp)
39
+ }
40
+ source_location = event_field(event, :source_location)
41
+ payload["source_location"] = hash_or_empty(source_location) if source_location
42
+
43
+ LineEncoder.to_json_line(Sanitizer.sanitize_hash(payload))
44
+ end
45
+
46
+ def encode_fallback(event, error)
47
+ event_name = begin
48
+ Helpers.safe_string(event && event_field(event, :name))
49
+ rescue
50
+ nil
51
+ end
52
+
53
+ LineEncoder.to_json_line(
54
+ Sanitizer.sanitize_hash(
55
+ {
56
+ "name" => "json_logging.event_encode_error",
57
+ "event_name" => event_name,
58
+ "error" => {
59
+ "class" => error.class.name,
60
+ "message" => Helpers.safe_string(error.message)
61
+ }
62
+ }
63
+ )
64
+ )
65
+ end
66
+
67
+ def event_field(event, key)
68
+ return nil unless event.is_a?(Hash)
69
+
70
+ if event.key?(key)
71
+ event[key]
72
+ elsif event.key?(key.to_s)
73
+ event[key.to_s]
74
+ end
75
+ end
76
+
77
+ def serialize_payload(payload)
78
+ case payload
79
+ when Hash
80
+ payload
81
+ when nil
82
+ {}
83
+ else
84
+ serialize_object(payload)
85
+ end
86
+ end
87
+
88
+ def serialize_tags(tags)
89
+ return {} unless tags.is_a?(Hash)
90
+
91
+ tags.transform_values { |value| serialize_tag_value(value) }
92
+ end
93
+
94
+ def serialize_tag_value(value)
95
+ case value
96
+ when Hash, TrueClass, FalseClass, Numeric, NilClass, String
97
+ value
98
+ else
99
+ serialize_object(value)
100
+ end
101
+ end
102
+
103
+ def serialize_object(object)
104
+ if object.respond_to?(:serialize)
105
+ serialized = object.serialize
106
+ serialized.is_a?(Hash) ? serialized : {"value" => serialized}
107
+ elsif object.respond_to?(:to_h)
108
+ object.to_h
109
+ else
110
+ {
111
+ "class" => object.class.name,
112
+ "value" => Helpers.safe_string(object)
113
+ }
114
+ end
115
+ end
116
+
117
+ def hash_or_empty(value)
118
+ value.is_a?(Hash) ? value : {}
119
+ end
120
+
121
+ def write_line(line)
122
+ if @io
123
+ @io.write(line)
124
+ else
125
+ resolve_logger << line
126
+ end
127
+ end
128
+
129
+ def resolve_logger
130
+ logger = @logger
131
+ # Proc responds to << (composition); Logger responds to add.
132
+ return logger.call if logger.respond_to?(:call) && !logger.respond_to?(:add)
133
+
134
+ logger
135
+ end
136
+
137
+ def report_write_error(error)
138
+ return unless defined?(ActiveSupport) && ActiveSupport.respond_to?(:error_reporter)
139
+ return if ActiveSupport.error_reporter.nil?
140
+
141
+ ActiveSupport.error_reporter.report(error, handled: true, severity: :error)
142
+ rescue
143
+ nil
144
+ end
145
+ end
146
+ end
@@ -8,18 +8,16 @@ module JsonLogging
8
8
 
9
9
  def initialize(*args, **kwargs)
10
10
  # Initialize with minimal args to avoid ActiveSupport::Logger threading issues
11
- # Rails 7+ supports kwargs, Rails 6 uses positional args only
12
11
  logdev = args.first || $stdout
13
12
  shift_age = args[1] || 0
14
13
  shift_size = args[2]
15
14
 
16
- # Handle both positional and keyword arguments for Rails 6-8 compatibility
15
+ # Handle both positional and keyword arguments for Rails 7–8 compatibility
17
16
  if kwargs.empty? && shift_size
18
17
  super(logdev, shift_age, shift_size)
19
18
  elsif kwargs.empty?
20
19
  super(logdev, shift_age)
21
20
  else
22
- # Rails 7+ may pass kwargs - delegate to parent
23
21
  super
24
22
  end
25
23
 
@@ -95,7 +95,7 @@ module JsonLogging
95
95
 
96
96
  def current_tags
97
97
  # Use IsolatedExecutionState (Rails 7.1+) for better thread/Fiber safety
98
- # Falls back to Thread.current for Rails 6-7.0
98
+ # Falls back to Thread.current for Rails 7.0
99
99
  if defined?(ActiveSupport::IsolatedExecutionState)
100
100
  ActiveSupport::IsolatedExecutionState[tags_key] ||= []
101
101
  else
@@ -105,7 +105,7 @@ module JsonLogging
105
105
 
106
106
  def set_tags(new_tags)
107
107
  # Use IsolatedExecutionState (Rails 7.1+) for better thread/Fiber safety
108
- # Falls back to Thread.current for Rails 6-7.0
108
+ # Falls back to Thread.current for Rails 7.0
109
109
  if defined?(ActiveSupport::IsolatedExecutionState)
110
110
  ActiveSupport::IsolatedExecutionState[tags_key] = new_tags
111
111
  else
@@ -131,7 +131,7 @@ module JsonLogging
131
131
  if filter
132
132
  # ParameterFilter will filter based on Rails.config.filter_parameters
133
133
  # This includes encrypted attributes automatically
134
- # Create a deep copy since filter modifies in place (Rails 6+)
134
+ # Create a deep copy since filter modifies in place
135
135
  filtered = limited_hash.respond_to?(:deep_dup) ? limited_hash.deep_dup : limited_hash.dup
136
136
  filtered = filter.filter(filtered)
137
137
 
@@ -1,7 +1,7 @@
1
1
  module JsonLogging
2
2
  module Sanitizer
3
3
  module StructuredHash
4
- JsonableResult = Struct.new(:tree, :owned, :leaf_count, keyword_init: true)
4
+ JsonableResult = Struct.new(:tree, :owned, :leaf_count)
5
5
 
6
6
  module_function
7
7
 
@@ -99,14 +99,14 @@ module JsonLogging
99
99
  when String
100
100
  leaf_counter[0] += 1 if leaf_counter
101
101
  sanitized = Sanitizer.sanitize_string(value)
102
- JsonableResult.new(tree: sanitized, owned: sanitized != value)
102
+ JsonableResult.new(tree: sanitized, owned: sanitized != value, leaf_count: nil)
103
103
  when Hash
104
104
  jsonable_tree(value, depth: depth + 1, seen: seen, parent_key: parent_key, leaf_counter: leaf_counter)
105
105
  when Array
106
106
  jsonable_array(value, depth: depth, seen: seen, parent_key: parent_key, leaf_counter: leaf_counter)
107
107
  else
108
108
  leaf_counter[0] += 1 if leaf_counter
109
- JsonableResult.new(tree: value, owned: false)
109
+ JsonableResult.new(tree: value, owned: false, leaf_count: nil)
110
110
  end
111
111
  end
112
112
 
@@ -123,7 +123,7 @@ module JsonLogging
123
123
  result[index] = child.tree
124
124
  end
125
125
 
126
- JsonableResult.new(tree: owned ? result : array, owned: owned)
126
+ JsonableResult.new(tree: owned ? result : array, owned: owned, leaf_count: nil)
127
127
  end
128
128
 
129
129
  def process_jsonable_entry(key, value, depth:, seen:, parent_key:, omit_keys:, leaf_counter:)
@@ -1,3 +1,3 @@
1
1
  module JsonLogging
2
- VERSION = "1.2.2"
2
+ VERSION = "1.3.0"
3
3
  end
data/lib/json_logging.rb CHANGED
@@ -12,6 +12,7 @@ require_relative "json_logging/formatter"
12
12
  require_relative "json_logging/formatter_with_tags"
13
13
  require_relative "json_logging/json_logger_extension"
14
14
  require_relative "json_logging/json_logger"
15
+ require_relative "json_logging/event_subscriber"
15
16
 
16
17
  module JsonLogging
17
18
  THREAD_CONTEXT_KEY = :__json_logging_context
data/sig/json_logging.rbs CHANGED
@@ -1,3 +1,8 @@
1
1
  module JsonLogging
2
2
  VERSION: String
3
+
4
+ class EventSubscriber
5
+ def initialize: (?logger: untyped, ?io: untyped) -> void
6
+ def emit: (untyped event) -> void
7
+ end
3
8
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: activesupport-json_logging
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.2.2
4
+ version: 1.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andrei Makarov
@@ -15,7 +15,7 @@ dependencies:
15
15
  requirements:
16
16
  - - ">="
17
17
  - !ruby/object:Gem::Version
18
- version: '6.0'
18
+ version: '7.0'
19
19
  - - "<"
20
20
  - !ruby/object:Gem::Version
21
21
  version: '9.0'
@@ -25,7 +25,7 @@ dependencies:
25
25
  requirements:
26
26
  - - ">="
27
27
  - !ruby/object:Gem::Version
28
- version: '6.0'
28
+ version: '7.0'
29
29
  - - "<"
30
30
  - !ruby/object:Gem::Version
31
31
  version: '9.0'
@@ -35,7 +35,7 @@ dependencies:
35
35
  requirements:
36
36
  - - ">="
37
37
  - !ruby/object:Gem::Version
38
- version: '6.0'
38
+ version: '7.0'
39
39
  - - "<"
40
40
  - !ruby/object:Gem::Version
41
41
  version: '9.0'
@@ -45,7 +45,7 @@ dependencies:
45
45
  requirements:
46
46
  - - ">="
47
47
  - !ruby/object:Gem::Version
48
- version: '6.0'
48
+ version: '7.0'
49
49
  - - "<"
50
50
  - !ruby/object:Gem::Version
51
51
  version: '9.0'
@@ -316,7 +316,7 @@ dependencies:
316
316
  - !ruby/object:Gem::Version
317
317
  version: '0.2'
318
318
  description: Lightweight JSON logger and formatter integrating with Rails/ActiveSupport.
319
- No extra deps beyond Rails/Activesupport. Compatible with Rails 6–8.
319
+ No extra deps beyond Rails/Activesupport. Compatible with Rails 7–8.
320
320
  email:
321
321
  - contact@kiskolabs.com
322
322
  executables: []
@@ -329,6 +329,7 @@ files:
329
329
  - lib/activesupport/json_logging.rb
330
330
  - lib/activesupport/json_logging/railtie.rb
331
331
  - lib/json_logging.rb
332
+ - lib/json_logging/event_subscriber.rb
332
333
  - lib/json_logging/formatter.rb
333
334
  - lib/json_logging/formatter_with_tags.rb
334
335
  - lib/json_logging/helpers.rb
@@ -357,14 +358,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
357
358
  requirements:
358
359
  - - ">="
359
360
  - !ruby/object:Gem::Version
360
- version: '2.7'
361
+ version: '3.2'
361
362
  required_rubygems_version: !ruby/object:Gem::Requirement
362
363
  requirements:
363
364
  - - ">="
364
365
  - !ruby/object:Gem::Version
365
366
  version: '0'
366
367
  requirements: []
367
- rubygems_version: 4.0.3
368
+ rubygems_version: 4.0.6
368
369
  specification_version: 4
369
370
  summary: Structured JSON logging for Rails/ActiveSupport with safe, single-line entries.
370
371
  test_files: []