activesupport-json_logging 1.1.0 → 1.2.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: 4854135797f8609d12de7e48f10b2ece4151e828ae9d6699fcd8f367f645cf5f
4
- data.tar.gz: 7fe2c2710e8adcab432233e3c55f97d337ba89ca1d5bd286c4e8b290662a3ac7
3
+ metadata.gz: c99bef73cc6bf2627075656ee38a3e3b756ac61ef54f2edcc628814b317ac7bf
4
+ data.tar.gz: c4a0e1ecfd0ae8862db0167cc5507b8da4572e6a58ed4035ec7aa606c3154442
5
5
  SHA512:
6
- metadata.gz: bd806fd601a0c4f761bd235f46e848f16701fe09e2995039c22e07c4aaa26545ad15f756ec635da3768a20a678fe7d3f6f35d2535a98f482fe885137758c1a58
7
- data.tar.gz: 72c2460e491be67a3ead7de92e549cf11d13c51b3da774251080263311b51c3c8a79a7bc0ad4354b9bb2b0d11d16ae47f74511b800f1230b2fb5aaf3adc68b8c
6
+ metadata.gz: 3abb724226828b28519f25dc8edb138c3b4d7dc1a07bf2946b952e46bee4bef5d71149a44df4a52cb12e41f892ca4d2f7147bfdeb9dbd7ad0ca3c4ee3873dddc
7
+ data.tar.gz: 25b2c9f5ae0c8107e4f5c7385ff96fbb379cbfba851234be9800909978b14d1d81b5fab961a5d72bd1d4ede78c5d1d16eb5ab1534e6ed54d2074e1c21456395c
data/CHANGELOG.md CHANGED
@@ -1,5 +1,12 @@
1
1
  # CHANGELOG
2
2
 
3
+ ## 1.2.0 (2025-11-07)
4
+
5
+ - Add support for service-specific tagged loggers: create loggers with permanent tags using `logger.tagged("service")` without a block
6
+ - Improve BroadcastLogger compatibility: service-specific loggers work seamlessly with `ActiveSupport::BroadcastLogger`
7
+ - Fix LocalTagStorage implementation to match Rails' TaggedLogging behavior: use `tag_stack` attribute accessor pattern for proper tag isolation
8
+ - Add comprehensive examples in README for service-specific loggers and BroadcastLogger integration
9
+
3
10
  ## 1.1.0 (2025-11-04)
4
11
 
5
12
  - Move tags to root level of JSON payload instead of nested in context (breaking change: tags now at `payload["tags"]` instead of `payload["context"]["tags"]`)
data/README.md CHANGED
@@ -1,6 +1,6 @@
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/ci.yml/badge.svg)](https://github.com/amkisko/activesupport-json_logging.rb/actions/workflows/ci.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)
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)
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.
@@ -8,6 +8,10 @@ Supports Rails versions from 6 to 8.
8
8
 
9
9
  Sponsored by [Kisko Labs](https://www.kiskolabs.com).
10
10
 
11
+ <a href="https://www.kiskolabs.com">
12
+ <img src="kisko.svg" width="200" alt="Sponsored by Kisko Labs" />
13
+ </a>
14
+
11
15
 
12
16
  ## Installation
13
17
 
@@ -40,6 +44,8 @@ end
40
44
  - `JsonLogging.with_context` to attach contextual fields per-thread
41
45
  - Smart message parsing (handles hashes, JSON strings, plain strings, and Exception objects)
42
46
  - Native `tagged` method support - use it just like Rails' tagged logger
47
+ - Service-specific tagged loggers - create loggers with permanent tags using `logger.tagged("service")` without a block
48
+ - Full compatibility with `ActiveSupport::BroadcastLogger` (Rails 7.1+)
43
49
  - Automatic Rails integration via Railtie (auto-requires the gem in Rails apps)
44
50
 
45
51
  ## Basic usage
@@ -63,6 +69,17 @@ logger.tagged("BCX").info("Stuff")
63
69
  logger.tagged("BCX", "Jason").info("Stuff")
64
70
  logger.tagged("BCX").tagged("Jason").info("Stuff")
65
71
 
72
+ # Create a service-specific logger with permanent tags
73
+ # All logs from this logger will include the "dotenv" tag
74
+ dotenv_logger = logger.tagged("dotenv")
75
+ dotenv_logger.info("Loading environment variables") # Includes "dotenv" tag
76
+ dotenv_logger.warn("Missing .env file") # Includes "dotenv" tag
77
+
78
+ # You can also create service loggers directly
79
+ base_logger = JsonLogging.logger($stdout)
80
+ service_logger = base_logger.tagged("my-service")
81
+ service_logger.info("Service started") # All logs tagged with "my-service"
82
+
66
83
  # Add context
67
84
  JsonLogging.with_context(user_id: 123) do
68
85
  logger.warn({event: "slow_query", duration_ms: 250})
@@ -85,6 +102,84 @@ This gem does **not** automatically configure your Rails app. You set it up manu
85
102
  - In Rails 7.1+, Rails automatically wraps your logger in `ActiveSupport::BroadcastLogger` to enable writing to multiple destinations (e.g., STDOUT and file simultaneously). This works seamlessly with our logger - your JSON logger will be wrapped and all method calls will delegate correctly. No special handling needed.
86
103
  - In Rails 7.1+, tag storage uses `ActiveSupport::IsolatedExecutionState` for improved thread/Fiber safety.
87
104
 
105
+ ### Service-specific loggers with tags
106
+
107
+ You can create loggers with permanent tags for specific services or components. This is useful when you want all logs from a particular service to be tagged consistently:
108
+
109
+ ```ruby
110
+ # Create a logger for DotEnv service with "dotenv" tag
111
+ base_logger = JsonLogging.logger($stdout)
112
+ dotenv_logger = base_logger.tagged("dotenv")
113
+
114
+ # All logs from this logger will include the "dotenv" tag
115
+ dotenv_logger.info("Loading .env file")
116
+ dotenv_logger.warn("Missing .env.local file")
117
+ dotenv_logger.error("Invalid environment variable format")
118
+
119
+ # Example: Configure Dotenv::Rails to use tagged logger
120
+ if defined?(Dotenv::Rails)
121
+ Dotenv::Rails.logger = base_logger.tagged("dotenv")
122
+ end
123
+
124
+ # Example: Create multiple service loggers
125
+ redis_logger = base_logger.tagged("redis")
126
+ sidekiq_logger = base_logger.tagged("sidekiq")
127
+ api_logger = base_logger.tagged("api")
128
+
129
+ # Each service logger maintains its tag across all log calls
130
+ redis_logger.info("Connected to Redis") # Tagged with "redis"
131
+ sidekiq_logger.info("Job enqueued") # Tagged with "sidekiq"
132
+ api_logger.info("Request received") # Tagged with "api"
133
+ ```
134
+
135
+ ### BroadcastLogger integration
136
+
137
+ `ActiveSupport::BroadcastLogger` (Rails 7.1+) allows writing logs to multiple destinations simultaneously. `JsonLogging` works seamlessly with `BroadcastLogger`:
138
+
139
+ ```ruby
140
+ # Create JSON loggers for different destinations
141
+ stdout_logger = JsonLogging.logger($stdout)
142
+ file_logger = JsonLogging.logger(Rails.root.join("log", "production.log"))
143
+
144
+ # Wrap in BroadcastLogger to write to both destinations
145
+ broadcast_logger = ActiveSupport::BroadcastLogger.new(stdout_logger)
146
+ broadcast_logger.broadcast_to(file_logger)
147
+
148
+ # All logging methods work through BroadcastLogger
149
+ broadcast_logger.info("This goes to both STDOUT and file")
150
+ broadcast_logger.warn({event: "warning", message: "Something happened"})
151
+
152
+ # Tagged logging works through BroadcastLogger
153
+ broadcast_logger.tagged("REQUEST", request_id) do
154
+ broadcast_logger.info("Processing request") # Tagged logs go to both destinations
155
+ end
156
+
157
+ # Service-specific loggers work with BroadcastLogger
158
+ # Note: Create service logger from underlying logger, then wrap in BroadcastLogger
159
+ # (BroadcastLogger.tagged without block returns array due to delegation)
160
+ base_logger = JsonLogging.logger($stdout)
161
+ dotenv_logger = base_logger.tagged("dotenv")
162
+ dotenv_broadcast = ActiveSupport::BroadcastLogger.new(dotenv_logger)
163
+ dotenv_broadcast.broadcast_to(file_logger.tagged("dotenv")) # Tag second destination too
164
+ dotenv_broadcast.info("Environment loaded") # Tagged and broadcast to all destinations
165
+
166
+ # Rails 7.1+ automatically uses BroadcastLogger
167
+ # Your configuration can be simplified:
168
+ Rails.application.configure do
169
+ # Rails will automatically wrap this in BroadcastLogger
170
+ base_logger = ActiveSupport::Logger.new($stdout)
171
+ json_logger = JsonLogging.new(base_logger)
172
+ config.logger = json_logger # Rails wraps this in BroadcastLogger automatically
173
+ end
174
+ ```
175
+
176
+ **Key points:**
177
+ - All logger methods (`info`, `warn`, `error`, etc.) work through `BroadcastLogger`
178
+ - Tagged logging (`tagged`) works correctly through `BroadcastLogger`
179
+ - Service-specific tagged loggers work with `BroadcastLogger`
180
+ - Each destination receives properly formatted JSON logs
181
+ - No special configuration needed - just wrap your `JsonLogging` logger in `BroadcastLogger`
182
+
88
183
  ### Basic setup
89
184
 
90
185
  Create `config/initializers/json_logging.rb`:
@@ -502,7 +597,7 @@ The gem will automatically filter these from all log entries, including context
502
597
  ```bash
503
598
  # Install dependencies
504
599
  bundle install
505
- bundle exec appraisal install
600
+ bundle exec appraisal generate
506
601
 
507
602
  # Run tests for current Rails version
508
603
  bundle exec rspec
@@ -8,7 +8,13 @@ module JsonLogging
8
8
  end
9
9
 
10
10
  def current_tags
11
- @logger.send(:current_tags)
11
+ # If LocalTagStorage is extended on this formatter, use its tag_stack
12
+ # This matches Rails' TaggedLogging behavior where tag_stack attribute shadows the method
13
+ if respond_to?(:tag_stack, true) && instance_variable_defined?(:@tag_stack)
14
+ tag_stack.tags
15
+ else
16
+ @logger.send(:current_tags)
17
+ end
12
18
  end
13
19
 
14
20
  def call(severity, timestamp, progname, msg)
@@ -22,18 +28,42 @@ module JsonLogging
22
28
  build_fallback_output(severity, timestamp, msg, e)
23
29
  end
24
30
 
31
+ def push_tags(*tags)
32
+ # If LocalTagStorage is present, use it; otherwise use logger's thread-local storage
33
+ if respond_to?(:tag_stack, true) && instance_variable_defined?(:@tag_stack)
34
+ tag_stack.push_tags(tags)
35
+ else
36
+ @logger.send(:push_tags, tags)
37
+ end
38
+ end
39
+
25
40
  # Support tagged blocks for formatter
26
41
  def tagged(*tags)
27
42
  if block_given?
28
- previous = @logger.send(:current_tags).dup
29
- @logger.send(:push_tags, tags)
30
- begin
31
- yield @logger
32
- ensure
33
- @logger.send(:set_tags, previous)
43
+ # If LocalTagStorage is present, use it; otherwise use logger's thread-local storage
44
+ if respond_to?(:tag_stack, true) && instance_variable_defined?(:@tag_stack)
45
+ previous_count = tag_stack.tags.size
46
+ tag_stack.push_tags(tags)
47
+ begin
48
+ yield @logger
49
+ ensure
50
+ tag_stack.pop_tags(tag_stack.tags.size - previous_count)
51
+ end
52
+ else
53
+ previous = @logger.send(:current_tags).dup
54
+ @logger.send(:push_tags, tags)
55
+ begin
56
+ yield @logger
57
+ ensure
58
+ @logger.send(:set_tags, previous)
59
+ end
34
60
  end
35
61
  else
36
- @logger.send(:push_tags, tags)
62
+ if respond_to?(:tag_stack, true) && instance_variable_defined?(:@tag_stack)
63
+ tag_stack.push_tags(tags)
64
+ else
65
+ @logger.send(:push_tags, tags)
66
+ end
37
67
  self
38
68
  end
39
69
  end
@@ -75,7 +75,9 @@ module JsonLogging
75
75
  # Return a new wrapped logger with tags applied (similar to TaggedLogging)
76
76
  logger = JsonLogging.new(self)
77
77
  # Extend formatter with LocalTagStorage to preserve current tags when creating nested loggers
78
+ # This matches Rails' TaggedLogging behavior
78
79
  logger.formatter.extend(LocalTagStorage)
80
+ # Push tags through formatter (matches Rails delegation pattern)
79
81
  logger.formatter.push_tags(*formatter.current_tags, *tags)
80
82
  logger
81
83
  end
@@ -132,7 +134,7 @@ module JsonLogging
132
134
  payload = PayloadBuilder.merge_context(
133
135
  payload,
134
136
  additional_context: JsonLogging.additional_context.compact,
135
- tags: current_tags
137
+ tags: formatter.current_tags
136
138
  )
137
139
 
138
140
  payload.compact
@@ -159,19 +161,37 @@ module JsonLogging
159
161
  # Module for preserving current tags when creating nested tagged loggers
160
162
  # Similar to ActiveSupport::TaggedLogging::LocalTagStorage
161
163
  # When extended on a formatter, stores tags locally instead of using thread-local storage
164
+ # Uses tag_stack attribute accessor pattern to match Rails' TaggedLogging behavior
162
165
  module LocalTagStorage
166
+ attr_accessor :tag_stack
167
+
163
168
  def self.extended(base)
164
- base.instance_variable_set(:@local_tags, [])
169
+ base.tag_stack = LocalTagStack.new
165
170
  end
166
171
 
167
- def push_tags(*tags)
168
- flat = tags.flatten.compact.map(&:to_s).reject(&:empty?)
169
- return if flat.empty?
170
- @local_tags = (@local_tags || []) + flat
171
- end
172
+ # Simple tag stack implementation for local tag storage
173
+ # Similar to ActiveSupport::TaggedLogging::TagStack but simplified for JSON logging
174
+ class LocalTagStack
175
+ attr_reader :tags
172
176
 
173
- def current_tags
174
- @local_tags || []
177
+ def initialize
178
+ @tags = []
179
+ end
180
+
181
+ def push_tags(tags)
182
+ flat = Array(tags).flatten.compact.map(&:to_s).reject(&:empty?)
183
+ return [] if flat.empty?
184
+ @tags.concat(flat)
185
+ flat
186
+ end
187
+
188
+ def pop_tags(count = 1)
189
+ @tags.pop(count)
190
+ end
191
+
192
+ def clear
193
+ @tags.clear
194
+ end
175
195
  end
176
196
  end
177
197
  end
@@ -1,3 +1,3 @@
1
1
  module JsonLogging
2
- VERSION = "1.1.0"
2
+ VERSION = "1.2.0"
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: activesupport-json_logging
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.1.0
4
+ version: 1.2.0
5
5
  platform: ruby
6
6
  authors:
7
- - amkisko
7
+ - Andrei Makarov
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2025-11-04 00:00:00.000000000 Z
11
+ date: 2025-11-10 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -56,42 +56,56 @@ dependencies:
56
56
  requirements:
57
57
  - - "~>"
58
58
  - !ruby/object:Gem::Version
59
- version: '3.12'
59
+ version: '3'
60
+ type: :development
61
+ prerelease: false
62
+ version_requirements: !ruby/object:Gem::Requirement
63
+ requirements:
64
+ - - "~>"
65
+ - !ruby/object:Gem::Version
66
+ version: '3'
67
+ - !ruby/object:Gem::Dependency
68
+ name: webmock
69
+ requirement: !ruby/object:Gem::Requirement
70
+ requirements:
71
+ - - "~>"
72
+ - !ruby/object:Gem::Version
73
+ version: '3'
60
74
  type: :development
61
75
  prerelease: false
62
76
  version_requirements: !ruby/object:Gem::Requirement
63
77
  requirements:
64
78
  - - "~>"
65
79
  - !ruby/object:Gem::Version
66
- version: '3.12'
80
+ version: '3'
67
81
  - !ruby/object:Gem::Dependency
68
82
  name: rake
69
83
  requirement: !ruby/object:Gem::Requirement
70
84
  requirements:
71
85
  - - "~>"
72
86
  - !ruby/object:Gem::Version
73
- version: '13.0'
87
+ version: '13'
74
88
  type: :development
75
89
  prerelease: false
76
90
  version_requirements: !ruby/object:Gem::Requirement
77
91
  requirements:
78
92
  - - "~>"
79
93
  - !ruby/object:Gem::Version
80
- version: '13.0'
94
+ version: '13'
81
95
  - !ruby/object:Gem::Dependency
82
96
  name: simplecov
83
97
  requirement: !ruby/object:Gem::Requirement
84
98
  requirements:
85
99
  - - "~>"
86
100
  - !ruby/object:Gem::Version
87
- version: '0.21'
101
+ version: '0.22'
88
102
  type: :development
89
103
  prerelease: false
90
104
  version_requirements: !ruby/object:Gem::Requirement
91
105
  requirements:
92
106
  - - "~>"
93
107
  - !ruby/object:Gem::Version
94
- version: '0.21'
108
+ version: '0.22'
95
109
  - !ruby/object:Gem::Dependency
96
110
  name: rspec_junit_formatter
97
111
  requirement: !ruby/object:Gem::Requirement
@@ -126,42 +140,56 @@ dependencies:
126
140
  requirements:
127
141
  - - "~>"
128
142
  - !ruby/object:Gem::Version
129
- version: '1.0'
143
+ version: '1'
130
144
  type: :development
131
145
  prerelease: false
132
146
  version_requirements: !ruby/object:Gem::Requirement
133
147
  requirements:
134
148
  - - "~>"
135
149
  - !ruby/object:Gem::Version
136
- version: '1.0'
150
+ version: '1'
137
151
  - !ruby/object:Gem::Dependency
138
152
  name: appraisal
139
153
  requirement: !ruby/object:Gem::Requirement
140
154
  requirements:
141
155
  - - "~>"
142
156
  - !ruby/object:Gem::Version
143
- version: '2.4'
157
+ version: '2'
144
158
  type: :development
145
159
  prerelease: false
146
160
  version_requirements: !ruby/object:Gem::Requirement
147
161
  requirements:
148
162
  - - "~>"
149
163
  - !ruby/object:Gem::Version
150
- version: '2.4'
164
+ version: '2'
151
165
  - !ruby/object:Gem::Dependency
152
166
  name: memory_profiler
153
167
  requirement: !ruby/object:Gem::Requirement
154
168
  requirements:
155
169
  - - "~>"
156
170
  - !ruby/object:Gem::Version
157
- version: '1.0'
171
+ version: '1'
172
+ type: :development
173
+ prerelease: false
174
+ version_requirements: !ruby/object:Gem::Requirement
175
+ requirements:
176
+ - - "~>"
177
+ - !ruby/object:Gem::Version
178
+ version: '1'
179
+ - !ruby/object:Gem::Dependency
180
+ name: rbs
181
+ requirement: !ruby/object:Gem::Requirement
182
+ requirements:
183
+ - - "~>"
184
+ - !ruby/object:Gem::Version
185
+ version: '3'
158
186
  type: :development
159
187
  prerelease: false
160
188
  version_requirements: !ruby/object:Gem::Requirement
161
189
  requirements:
162
190
  - - "~>"
163
191
  - !ruby/object:Gem::Version
164
- version: '1.0'
192
+ version: '3'
165
193
  description: Lightweight JSON logger and formatter integrating with Rails/ActiveSupport.
166
194
  No extra deps beyond Rails/Activesupport. Compatible with Rails 6–8.
167
195
  email: