json_tagged_logger 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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 9d9b6a0f1e36ce3cc9e4bc89ea942c6ecea3868d240f58230604c30c954232a2
4
+ data.tar.gz: 10acc0b50c43dd4d938920a315e11aad21b860022397f4e36273e98f3f4d9192
5
+ SHA512:
6
+ metadata.gz: 19ee0ae1fa6341aafb6443c21d805e6e787e7bcd5f8e7447a42a38b428925f47a88b101025e3beef34e2db1b74c968ce2ff819374897c8d2d848253cbb9c27c3
7
+ data.tar.gz: c5697f900381e3d33862b1ff750385ea4feb7a8e05723f719c1b3853a09f7d1336ae7a332caf3fa8b9684c3482019b2bfb319a4cd7ef9b35a5b9a1737addc9f9
data/LICENSE ADDED
@@ -0,0 +1,19 @@
1
+ Copyright (c) 2023 Sean Santry
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ of this software and associated documentation files (the "Software"), to deal
5
+ in the Software without restriction, including without limitation the rights
6
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ copies of the Software, and to permit persons to whom the Software is
8
+ furnished to do so, subject to the following conditions:
9
+
10
+ The above copyright notice and this permission notice shall be included in all
11
+ copies or substantial portions of the Software.
12
+
13
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,89 @@
1
+ # JsonTaggedLogger
2
+
3
+ JsonTaggedLogger provides a log formatter that works in conjunction with ActiveSupport::TaggedLogging to product JSON-formatted log output.
4
+
5
+
6
+ ```ruby
7
+ # Gemfile
8
+ gem 'json_tagged_logger'
9
+
10
+ # config/environments/production.rb
11
+ Rails.application.configure do
12
+ # …
13
+ config.log_tags = JsonTaggedLogger::LogTagsConfig.generate(:request_id, :host, ->(request) { { example: "proc" }.to_json })
14
+ logger = ActiveSupport::Logger.new(STDOUT)
15
+ config.logger = JsonTaggedLogger::Logger.new(logger)
16
+ # …
17
+ end
18
+
19
+ ```
20
+
21
+
22
+ ## Why?
23
+
24
+ On its own, ActiveSupport::TaggedLogging adds individual tags wrapped in squre brackets at the start of each line of log output, like so
25
+
26
+ ```
27
+ [tag1] [tag2] : Foo bar
28
+ ```
29
+
30
+ This is fine as far as it goes, but if you wish to use tagged logging with something like [Lograge](https://github.com/roidrage/lograge) to format your logs as JSON, there's no good way to get the tags into that json output, specially if any of your tags are generated by procs.
31
+
32
+ ## How does it work?
33
+
34
+ First, JsonTaggedLogger::LogTags helps you generate an array lof log tags suitable for passing to Rails.application.config.log_tags to produce JSON logs. For example, given
35
+
36
+ ```
37
+ Rails.application.config.log_tags = JsonTaggedLogger::LogTags.generate_config(:request_id, :host)
38
+ ```
39
+
40
+ ActiveSupport::TaggedLogging will see the following log tags
41
+
42
+ ```
43
+ [
44
+ -> (request) { { request_id: request.send(:request_id) }.to_json },
45
+ -> (request) { { host: request.send(:host) }.to_json }
46
+ ]
47
+ ```
48
+
49
+ Why is this an improvement over `Rails.application.config.log_tags = [:request_id, :host]`? Well, with the JsonTaggedLogger version, your logs will now contain
50
+
51
+ ```
52
+ [{"request_id":"c31c3658-5b76-40cd-b583-d0fcbdee0710"}] [{"host":"127.0.0.1"}] log message
53
+ ```
54
+
55
+ which `JsonTaggedLogger::Formatter` will turn into
56
+
57
+ ```json
58
+ {"request_id":"c31c3658-5b76-40cd-b583-d0fcbdee0710","host":"127.0.0.1","msg":"log message"}
59
+ ```
60
+
61
+ Lovely, eh?
62
+
63
+ The real advantage comes when you have more complicated procs that generage your log tags. So long as your proc produces a JSON string, `JsonTaggedLogger::Formatter` will be able to extract it from the tag and add it to the log hash.
64
+
65
+ ```ruby
66
+ class UserInfoLogTag
67
+ def self.user_info
68
+ lambda do |request|
69
+ user_info = build_user_info_from_session(request) # build user info from request by inspecting session, etc
70
+ { user_info: user_info }.to_json
71
+ end
72
+ end
73
+
74
+ private
75
+
76
+ def self.build_user_info_from_session(request)
77
+ # left as an exercise for the reader
78
+ end
79
+ end
80
+
81
+ Rails.application.config.log_tags = [ :request_id, :host, UserInfoLogTag.user_info ]
82
+ ```
83
+
84
+ That will get you
85
+
86
+ ```
87
+ {"request_id":"c31c3658-5b76-40cd-b583-d0fcbdee0710","host":"127.0.0.1","user_info":{"user_id":"1234","user_email":"user@example.com"},"msg":"log message"}
88
+ ```
89
+
@@ -0,0 +1,64 @@
1
+ module JsonTaggedLogger
2
+ class Formatter
3
+ def call(severity, _time, _progname, message)
4
+ log = {
5
+ level: severity,
6
+ }
7
+
8
+ json_tags, text_tags = extract_tags
9
+
10
+ json_tags.each { |t| log.merge!(t) }
11
+
12
+ if text_tags.present?
13
+ log[:tags] = text_tags
14
+ end
15
+
16
+ bare_message = message_without_tags(message)
17
+
18
+ begin
19
+ parsed_message = JSON.parse(bare_message)
20
+ rescue JSON::ParserError
21
+ parsed_message = bare_message
22
+ ensure
23
+ if parsed_message.is_a?(Hash)
24
+ log.merge!(parsed_message.symbolize_keys)
25
+ else
26
+ log.merge!(msg: parsed_message.strip)
27
+ end
28
+ end
29
+
30
+ log.compact.to_json + "\n"
31
+ end
32
+
33
+ private
34
+
35
+ def extract_tags
36
+ json_tags = Set[]
37
+ text_tags = Set[]
38
+
39
+ current_tags.each do |t|
40
+ begin
41
+ tag = JSON.parse(t)
42
+ rescue JSON::ParserError
43
+ tag = t
44
+ ensure
45
+ if tag.is_a?(Hash)
46
+ json_tags << tag
47
+ else
48
+ text_tags << tag
49
+ end
50
+ end
51
+ end
52
+
53
+ [json_tags, text_tags]
54
+ end
55
+
56
+ def message_without_tags(message)
57
+ if tags_text.present?
58
+ message.gsub(tags_text.strip, '')
59
+ else
60
+ message
61
+ end
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,15 @@
1
+ module JsonTaggedLogger
2
+ class LogTagsConfig
3
+ def self.generate(*tags)
4
+ tags.map do |tag|
5
+ if tag.is_a?(Proc) && tag.arity == 1
6
+ tag
7
+ elsif tag.is_a?(Symbol) && ActionDispatch::Request.method_defined?(tag)
8
+ -> (request) { { tag => request.send(tag) }.to_json }
9
+ else
10
+ raise ArgumentError, "Only symbols that ActionDispatch::Request responds to or single-argument Procs allowed. You provided '#{tag.inspect}'."
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,8 @@
1
+ module JsonTaggedLogger
2
+ module Logger
3
+ def self.new(logger)
4
+ logger.formatter = Formatter.new
5
+ ActiveSupport::TaggedLogging.new(logger)
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,25 @@
1
+ module JsonTaggedLogger
2
+ module TagFromSession
3
+ def self.get(key)
4
+ lambda do |request|
5
+ { key => get_value_from_session(request, key) }.to_json
6
+ end
7
+ end
8
+
9
+ private
10
+
11
+ def self.get_value_from_session(request, key)
12
+ session_options = Rails.application.config.session_options
13
+ session_store = Rails.application.config.session_store.new(Rails.application, session_options)
14
+ session = ActionDispatch::Request::Session.create(session_store, request, session_options)
15
+
16
+ session[key]
17
+ ensure
18
+ # Clean up side effects from loading the session so it can be loaded as
19
+ # usual during the normal request cycle. Leaving these headers in place
20
+ # seems to break some RSpec specs that interact with the session.
21
+ request.delete_header("rack.session")
22
+ request.delete_header("rack.session.options")
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,3 @@
1
+ module JsonTaggedLogger
2
+ VERSION = "0.1.0"
3
+ end
@@ -0,0 +1,4 @@
1
+ require 'json_tagged_logger/formatter'
2
+ require 'json_tagged_logger/log_tags_config'
3
+ require 'json_tagged_logger/logger'
4
+ require 'json_tagged_logger/tag_from_session'
metadata ADDED
@@ -0,0 +1,51 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: json_tagged_logger
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Sean Santry
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 1980-01-01 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description: Formatter for logging with ActiveSupport::TaggedLogging as JSON
14
+ email:
15
+ - sean@santry.us
16
+ executables: []
17
+ extensions: []
18
+ extra_rdoc_files: []
19
+ files:
20
+ - LICENSE
21
+ - README.md
22
+ - lib/json_tagged_logger.rb
23
+ - lib/json_tagged_logger/formatter.rb
24
+ - lib/json_tagged_logger/log_tags_config.rb
25
+ - lib/json_tagged_logger/logger.rb
26
+ - lib/json_tagged_logger/tag_from_session.rb
27
+ - lib/json_tagged_logger/version.rb
28
+ homepage: https://github.com/santry/json_tagged_logger
29
+ licenses:
30
+ - MIT
31
+ metadata: {}
32
+ post_install_message:
33
+ rdoc_options: []
34
+ require_paths:
35
+ - lib
36
+ required_ruby_version: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ required_rubygems_version: !ruby/object:Gem::Requirement
42
+ requirements:
43
+ - - ">="
44
+ - !ruby/object:Gem::Version
45
+ version: '0'
46
+ requirements: []
47
+ rubygems_version: 3.3.20
48
+ signing_key:
49
+ specification_version: 4
50
+ summary: JSON Tagged Log Formatter
51
+ test_files: []