json_tagged_logger 0.2.0 → 0.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: 3ddcb4231cfc04738123f81259ad35a28fe118f920dd5ab256f24b8edd76d4ac
4
- data.tar.gz: b44b6ee19cd6e763e9065aaa919145217025dcdc72408b38d577d4630017eb7e
3
+ metadata.gz: fde9c968aab725f59b4d0002b2e318b286edb8e8b7790222d1002e51ce631e6e
4
+ data.tar.gz: 97e23c6b02ec7faa8869659c59ef8ba1f2bc200cd607b4e248d2e8a7e74cab5f
5
5
  SHA512:
6
- metadata.gz: 8183f8c49929f51528b940e8111a68cc66762cd85d058f40fc0b5af21797e92439967442232dee6db8ce0937571b749a09cfc891f0cbc2f74f407648b6d4097d
7
- data.tar.gz: d81145bcdee2da86974f5c234aed2e033b63da4a2a80d1492ffa1f749a8cbd44b2e0d9c35e1b3b66f628adbfadbc68302072545cf341cd79cf23c30652d24285
6
+ metadata.gz: 2c21e9f1e126a80a8160587f04d30ca7ccd83433471e78c017320734d570f3cb43935e78bf8bd0a858d114c98fd2fc2115b45363f9e0e283d4c3d6ba182dd94e
7
+ data.tar.gz: be70dcda430a3341613c09c85eebcc66f98c9361658192808ab6fb9b0bd63b498713e8ff907fad6c6ce116dead29c302acb11c8bb51bf91edd5e8a6b3df880ab
data/README.md CHANGED
@@ -1,89 +1,141 @@
1
1
  # JsonTaggedLogger
2
2
 
3
- JsonTaggedLogger provides a log formatter that works in conjunction with ActiveSupport::TaggedLogging to product JSON-formatted log output.
3
+ [![Build Status](https://github.com/santry/json_tagged_logger/actions/workflows/ci.yml/badge.svg)](https://github.com/santry/json_tagged_logger/actions/workflows/ci.yml)
4
4
 
5
+ `JsonTaggedLogger` works in conjunction with [`ActiveSupport::TaggedLogging`](https://api.rubyonrails.org/classes/ActiveSupport/TaggedLogging.html) and (optionally) [Lograge](https://github.com/roidrage/lograge) to produce JSON-formatted log output. By itself, `ActiveSupport::TaggedLogging` supports simple tagging. With `JsonTaggedLogger`, you can compose key/value pairs, simple tags, and the log message itself into a single JSON document for easy consumption and parsing in log aggregators.
6
+
7
+ ## Usage
8
+
9
+ Given the following configuration,
5
10
 
6
11
  ```ruby
7
12
  # Gemfile
8
13
  gem 'json_tagged_logger'
14
+ gem 'lograge' # since you probably want all your request logs in JSON
9
15
 
10
16
  # config/environments/production.rb
11
17
  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
- #
18
+ # …
19
+ config.log_tags = JsonTaggedLogger::LogTagsConfig.generate(
20
+ :request_id,
21
+ :host,
22
+ ->(request) { { my_param: request.query_parameters["my_param"] }.to_json },
23
+ JsonTaggedLogger::TagFromSession.get(:user_id),
24
+ JsonTaggedLogger::TagFromHeaders.get(:my_custom_header, "X-Custom-Header"),
25
+ )
26
+
27
+ logger = ActiveSupport::Logger.new(STDOUT)
28
+ config.logger = JsonTaggedLogger::Logger.new(logger)
29
+
30
+ config.lograge.enabled = true
31
+ config.lograge.formatter = Lograge::Formatters::Json.new
32
+ # …
17
33
  end
18
-
19
34
  ```
20
35
 
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
36
+ A request like
25
37
 
26
38
  ```
27
- [tag1] [tag2] : Foo bar
39
+ curl -H "X-Custom-Header: some header value" 'http://127.0.0.1:3000/?my_param=param%20value'
28
40
  ```
29
41
 
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.
42
+ will get you something like
31
43
 
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)
44
+ ```json
45
+ {
46
+ "level": "INFO",
47
+ "request_id": "914f6104-538d-4ddc-beef-37bfe06ca1c7",
48
+ "host": "127.0.0.1",
49
+ "my_param": "param value",
50
+ "user_id": "99",
51
+ "my_custom_header": "some header value",
52
+ "method": "GET",
53
+ "path": "/",
54
+ "format": "*/*",
55
+ "controller": "SessionsController",
56
+ "action": "new",
57
+ "status": 302,
58
+ "duration": 0.51,
59
+ "view": 0,
60
+ "db": 0,
61
+ "location": "http://127.0.0.1:3000/profile"
62
+ }
38
63
  ```
39
64
 
40
- ActiveSupport::TaggedLogging will see the following log tags
65
+ [Note: I've pretty-printed the output in these examples for easier reading. The actual log output will be on a single line without extra whitespace.]
41
66
 
42
- ```
43
- [
44
- -> (request) { { request_id: request.send(:request_id) }.to_json },
45
- -> (request) { { host: request.send(:host) }.to_json }
46
- ]
67
+ Importantly, if the controller action (or any code it calls along the way) has an explicit call to `Rails.logger.tagged("TAG").info("tagged log message")`, you'll get the same key/value tags (`request_id`, `host`, `my_param`, &c.) in the JSON document along with a `tags` key:
68
+
69
+ ```json
70
+ {
71
+ "level": "INFO",
72
+ "request_id": "914f6104-538d-4ddc-beef-37bfe06ca1c7",
73
+ "host": "127.0.0.1",
74
+ "my_param": "param value",
75
+ "user_id": "99",
76
+ "my_custom_header": "some header value",
77
+ "tags": [
78
+ "TAG"
79
+ ],
80
+ "msg": "tagged log message"
81
+ }
47
82
  ```
48
83
 
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
84
+ If you have nested calls to the tagged logger, like
85
+
86
+ ```ruby
87
+ Rails.logger.tagged("TAG") do
88
+ Rails.logger.tagged("NESTED").info("nested tagged log message")
89
+ end
50
90
 
51
- ```
52
- [{"request_id":"c31c3658-5b76-40cd-b583-d0fcbdee0710"}] [{"host":"127.0.0.1"}] log message
53
91
  ```
54
92
 
55
- which `JsonTaggedLogger::Formatter` will turn into
93
+ those will be added to the `tags` key in the JSON document
56
94
 
57
95
  ```json
58
- {"request_id":"c31c3658-5b76-40cd-b583-d0fcbdee0710","host":"127.0.0.1","msg":"log message"}
96
+ {
97
+ "level": "INFO",
98
+ "request_id": "914f6104-538d-4ddc-beef-37bfe06ca1c7",
99
+ "host": "127.0.0.1",
100
+ "my_param": "param value",
101
+ "user_id": "99",
102
+ "my_custom_header": "some header value",
103
+ "tags": [
104
+ "TAG",
105
+ "NESTED"
106
+ ],
107
+ "msg": "nested tagged log message"
108
+ }
59
109
  ```
60
110
 
61
- Lovely, eh?
111
+ ## Why?
62
112
 
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.
113
+ On its own, `ActiveSupport::TaggedLogging` adds individual tags wrapped in squre brackets at the start of each line of log output. A configuration like
64
114
 
65
115
  ```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
116
+ Rails.application.configure do
117
+ #
118
+ config.log_tags = [ :request_id, :host, ->(request) { request.query_parameters["my_param"] } ]
119
+ logger = ActiveSupport::Logger.new(STDOUT)
120
+ config.logger = ActiveSupport::TaggedLogging.new(logger)
121
+ # …
79
122
  end
80
-
81
- Rails.application.config.log_tags = [ :request_id, :host, UserInfoLogTag.user_info ]
82
123
  ```
83
124
 
84
- That will get you
125
+ will get you logs like
85
126
 
86
127
  ```
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"}
128
+ [22a03298-5fd9-4b28-bbe2-1d4c0d7f74f0] [127.0.0.1] [param value] Started GET "/?my_param=param%20value" for 127.0.0.1 at 2023-01-15 00:13:21 -0500
129
+ [22a03298-5fd9-4b28-bbe2-1d4c0d7f74f0] [127.0.0.1] [param value] Processing by SessionsController#new as */*
130
+ [22a03298-5fd9-4b28-bbe2-1d4c0d7f74f0] [127.0.0.1] [param value] Parameters: {"my_param"=>"param value"}
131
+ [22a03298-5fd9-4b28-bbe2-1d4c0d7f74f0] [127.0.0.1] [param value] [TAG] [NESTED] nested tagged log message
132
+ [22a03298-5fd9-4b28-bbe2-1d4c0d7f74f0] [127.0.0.1] [param value] Redirected to http://127.0.0.1:3000/sign_in
133
+ [22a03298-5fd9-4b28-bbe2-1d4c0d7f74f0] [127.0.0.1] [param value] Completed 302 Found in 1ms (ActiveRecord: 0.0ms | Allocations: 1070)
88
134
  ```
89
135
 
136
+ This is fine as far as it goes, but if you use tagged logging with something like [Lograge](https://github.com/roidrage/lograge) to format your request logs as JSON, the list of square-bracketed tags will still just be prepended to the log line, followed by the JSON. Also, any calls to `Rails.logger.tagged` will be logged as plain text:
137
+
138
+ ```
139
+ [d4fac896-f916-48d7-9d32-d35a39cfb7d8] [127.0.0.1] [param value] [TAG] [NESTED] nested tagged log message
140
+ [d4fac896-f916-48d7-9d32-d35a39cfb7d8] [127.0.0.1] [param value] {"method":"GET","path":"/","format":"*/*","controller":"SessionsController","action":"new","status":302,"duration":1.2,"view":0.0,"db":0.0,"location":"http://127.0.0.1:3000/sign_in"}
141
+ ```
@@ -1,3 +1,6 @@
1
+ require 'active_support/core_ext/hash/keys'
2
+ require 'json'
3
+
1
4
  module JsonTaggedLogger
2
5
  class Formatter
3
6
  def call(severity, _time, _progname, message)
@@ -10,7 +13,7 @@ module JsonTaggedLogger
10
13
  json_tags.each { |t| log.merge!(t) }
11
14
 
12
15
  if text_tags.present?
13
- log[:tags] = text_tags
16
+ log[:tags] = text_tags.to_a
14
17
  end
15
18
 
16
19
  bare_message = message_without_tags(message)
@@ -21,8 +24,14 @@ module JsonTaggedLogger
21
24
  parsed_message = bare_message
22
25
  ensure
23
26
  if parsed_message.is_a?(Hash)
24
- log.merge!(parsed_message.symbolize_keys)
27
+ parsed_message.symbolize_keys!
28
+ if log.has_key?(:tags) && parsed_message.has_key?(:tags)
29
+ parsed_message[:tags] = parsed_message[:tags] + log[:tags]
30
+ end
31
+
32
+ log.merge!(parsed_message)
25
33
  else
34
+ #binding.irb
26
35
  log.merge!(msg: parsed_message.strip)
27
36
  end
28
37
  end
@@ -1,13 +1,17 @@
1
+ require 'action_dispatch'
2
+
1
3
  module JsonTaggedLogger
2
4
  class LogTagsConfig
3
5
  def self.generate(*tags)
4
6
  tags.map do |tag|
5
- if tag.is_a?(Proc) && tag.arity == 1
7
+ if tag.is_a?(String)
8
+ tag
9
+ elsif tag.is_a?(Proc) && tag.arity == 1
6
10
  tag
7
11
  elsif tag.is_a?(Symbol) && ActionDispatch::Request.method_defined?(tag)
8
12
  -> (request) { { tag => request.send(tag) }.to_json }
9
13
  else
10
- raise ArgumentError, "Only symbols that ActionDispatch::Request responds to or single-argument Procs allowed. You provided '#{tag.inspect}'."
14
+ raise ArgumentError, "Only strings, symbols that ActionDispatch::Request responds to or single-argument Procs allowed. You provided '#{tag.inspect}'."
11
15
  end
12
16
  end
13
17
  end
@@ -1,3 +1,6 @@
1
+ require 'active_support'
2
+ require 'active_support/tagged_logging'
3
+
1
4
  module JsonTaggedLogger
2
5
  module Logger
3
6
  def self.new(logger)
@@ -1,3 +1,5 @@
1
+ require 'action_dispatch'
2
+
1
3
  module JsonTaggedLogger
2
4
  module TagFromSession
3
5
  def self.get(log_label, session_key = log_label)
@@ -1,3 +1,3 @@
1
1
  module JsonTaggedLogger
2
- VERSION = "0.2.0"
2
+ VERSION = "0.3.0"
3
3
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: json_tagged_logger
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sean Santry
@@ -9,7 +9,77 @@ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
11
  date: 1980-01-01 00:00:00.000000000 Z
12
- dependencies: []
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '2.2'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '2.2'
27
+ - !ruby/object:Gem::Dependency
28
+ name: minitest
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '5.16'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '5.16'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rake
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '13.0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '13.0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: activesupport
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '6.1'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '6.1'
69
+ - !ruby/object:Gem::Dependency
70
+ name: actionpack
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '6.1'
76
+ type: :runtime
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '6.1'
13
83
  description: Formatter for logging with ActiveSupport::TaggedLogging as JSON
14
84
  email:
15
85
  - sean@santry.us
@@ -38,7 +108,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
38
108
  requirements:
39
109
  - - ">="
40
110
  - !ruby/object:Gem::Version
41
- version: '0'
111
+ version: '2.7'
42
112
  required_rubygems_version: !ruby/object:Gem::Requirement
43
113
  requirements:
44
114
  - - ">="