json_tagged_logger 0.2.0 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
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
  - - ">="