akita-har_logger 0.2.0 → 0.2.5

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: 3da46a0fcf41a0f5f1f1d88261e7345713fe9487d7a7c4485d85e0ee808c8717
4
- data.tar.gz: 8857a59375d849bf4c94b47a131c90a950f5bc06da6979c75815d3a6f1c0c4d9
3
+ metadata.gz: d1a952e08b6244134dc4498e24b7185a700fe6d60abdc257bae8e5c3f67a57c6
4
+ data.tar.gz: 205780425d013375017a8677630c68c078b3be55772ebc0bebf0968d8e16601b
5
5
  SHA512:
6
- metadata.gz: a2a17be7f105d38c3f75b70bd42dd8982c189a129895ca887333fc3609d2e3f1c362fddffd7a77f97bd43358bc907982ea6b718f96515297cf9b65c437073a0d
7
- data.tar.gz: 1919850a9c0be28cd22610699a17caec16bafa2b8bf9485ab4b0f7646fbf77a2b4c3294f676033ab6c14258bf52a7ffa08dd9fe3c7e0fe729c90fa228d2acfa2
6
+ metadata.gz: 0beadf21aa88256c40d0b6bc4c2e0c54dae7656aea1cd472b8ae7a610484969f70ed9239c3a783362eaf68be41c373085cb181eb456bce413510e8f50d5a50ed
7
+ data.tar.gz: 622997ac27c899f809b1d7c632ca0da6f8916f00a25b9cdb22d8d6861b1107c8226cfd7802c9b03745d774268780d066db5bcb739081a06c66cd89cee235016a
data/.gitignore ADDED
@@ -0,0 +1 @@
1
+ /pkg/
data/Gemfile.lock CHANGED
@@ -1,14 +1,12 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- akita-har_logger (0.2.0)
5
- json (~> 2.3)
4
+ akita-har_logger (0.2.5)
6
5
 
7
6
  GEM
8
7
  remote: https://rubygems.org/
9
8
  specs:
10
9
  diff-lcs (1.4.4)
11
- json (2.5.1)
12
10
  rake (13.0.3)
13
11
  rspec (3.10.0)
14
12
  rspec-core (~> 3.10.0)
@@ -33,4 +31,4 @@ DEPENDENCIES
33
31
  rspec (~> 3.10)
34
32
 
35
33
  BUNDLED WITH
36
- 2.2.15
34
+ 2.2.23
data/README.md CHANGED
@@ -48,6 +48,8 @@ Here is a sample configuration for a test environment that just adds the
48
48
  instrumentation.
49
49
 
50
50
  ```ruby
51
+ # config/environments/test.rb
52
+
51
53
  Rails.application.configure.do
52
54
  # Other configuration for the Rails application...
53
55
 
@@ -61,11 +63,30 @@ end
61
63
  ### `ActionController` filter
62
64
 
63
65
  To instrument with a filter, add an instance of `Akita::HarLogger::Filter` as
64
- an `around_action` filter to your `ActionController` implementation. Here is an
65
- example of a bare-bones `app/controllers/application_controller.rb` with this
66
- instrumentation.
66
+ an `around_action` filter to your `ActionController` implementation.
67
+
68
+ For convenience, you can call `Akita::HarLogger::Filter.install` to do this for
69
+ all `ActionController`s in your application. We recommend adding this call to a
70
+ configuration initializer. For example, this initializer adds the filter only
71
+ in the test environment:
67
72
 
68
73
  ```ruby
74
+ # config/initializers/har_logging.rb
75
+
76
+ # Add the HAR logger as an `around_action` filter to all `ActionControllers`
77
+ # that are loaded by the application in the test environment. Optionally give
78
+ # an output HAR file to save your trace. If not specified, this defaults to
79
+ # `akita_trace_{timestamp}.har`.
80
+ Akita::HarLogger::Filter.install("akita_trace.har") if Rails.env.test?
81
+ ```
82
+
83
+ You can also selectively instrument your `ActionController` implementations by
84
+ adding the filter manually. Here is a bare-bones `ActionController`
85
+ implementation that adds the filter only in the test environment.
86
+
87
+ ```ruby
88
+ # app/controllers/application_controller.rb
89
+
69
90
  class ApplicationController < ActionController::API
70
91
  include Response
71
92
  include ExceptionHandler
@@ -73,7 +94,7 @@ class ApplicationController < ActionController::API
73
94
  # Add the HAR logger as an `around_action` filter. Optionally give an output
74
95
  # HAR file to save your trace. If not specified, this defaults to
75
96
  # `akita_trace_{timestamp}.har`.
76
- around_action Akita::HarLogger::Filter.new("akita_trace.har")
97
+ around_action Akita::HarLogger::Filter.new("akita_trace.har") if Rails.env.test?
77
98
  end
78
99
  ```
79
100
 
@@ -31,7 +31,5 @@ Gem::Specification.new do |spec|
31
31
  spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
32
32
  spec.require_paths = ['lib']
33
33
 
34
- spec.add_dependency 'json', '~> 2.3'
35
-
36
34
  spec.add_development_dependency 'rspec', '~> 3.10'
37
35
  end
@@ -29,14 +29,10 @@ module Akita
29
29
  @app = app
30
30
 
31
31
  if out_file_name == nil then
32
- out_file_name = "akita_trace_#{Time.now.to_i}.har"
32
+ out_file_name = HarLogger.default_file_name
33
33
  end
34
34
 
35
- # This queue is used to ensure that event logging is thread-safe. The
36
- # main thread will enqueue HarEntry objects. The HAR writer thread
37
- # below dequeues these objects and writes them to the output file.
38
- @entry_queue = Queue.new
39
- WriterThread.new out_file_name, @entry_queue
35
+ @entry_queue = HarLogger.get_queue(out_file_name)
40
36
  end
41
37
 
42
38
  def call(env)
@@ -58,16 +54,21 @@ module Akita
58
54
  class Filter
59
55
  def initialize(out_file_name = nil)
60
56
  if out_file_name == nil then
61
- out_file_name = "akita_trace_#{Time.now.to_i}.har"
57
+ out_file_name = HarLogger.default_file_name
62
58
  end
63
59
 
64
- # This queue is used to ensure that event logging is thread-safe. The
65
- # main thread will enqueue HarEntry objects. The HAR writer thread
66
- # below dequeues these objects and writes them to the output file.
67
- @entry_queue = Queue.new
68
- WriterThread.new out_file_name, @entry_queue
60
+ @entry_queue = HarLogger.get_queue(out_file_name)
69
61
  end
70
62
 
63
+ # Registers an `on_load` initializer to add a logging filter to any
64
+ # ActionController that is created.
65
+ def self.install(out_file_name = nil, hook_name = :action_controller)
66
+ ActiveSupport.on_load(hook_name) do
67
+ around_action Filter.new(out_file_name)
68
+ end
69
+ end
70
+
71
+ # Implements the actual `around` filter.
71
72
  def around(controller)
72
73
  start_time = Time.now
73
74
 
@@ -84,5 +85,35 @@ module Akita
84
85
  [response.body])
85
86
  end
86
87
  end
88
+
89
+ @@default_file_name = "akita_trace_#{Time.now.to_i}.har"
90
+ def self.default_file_name
91
+ @@default_file_name
92
+ end
93
+
94
+ # Maps the name of each output file to a queue of entries to be logged to
95
+ # that file. The queue is used to ensure that event logging is thread-safe.
96
+ # The main thread will enqueue HarEntry objects. A HAR writer thread
97
+ # dequeues these objects and writes them to the output file.
98
+ @@entry_queues = {}
99
+ @@entry_queues_mutex = Mutex.new
100
+
101
+ # Returns the entry queue for the given file. If an entry queue doesn't
102
+ # already exist, one is created and a HAR writer thread is started for the
103
+ # queue.
104
+ def self.get_queue(out_file_name)
105
+ queue = nil
106
+ @@entry_queues_mutex.synchronize {
107
+ if @@entry_queues.has_key?(out_file_name) then
108
+ return @@entry_queues[out_file_name]
109
+ end
110
+
111
+ queue = Queue.new
112
+ @@entry_queues[out_file_name] = queue
113
+ }
114
+
115
+ WriterThread.new out_file_name, queue
116
+ return queue
117
+ end
87
118
  end
88
119
  end
@@ -1,6 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'json'
4
3
  require_relative 'http_request'
5
4
  require_relative 'http_response'
6
5
 
@@ -3,17 +3,46 @@
3
3
  module Akita
4
4
  module HarLogger
5
5
  class HarUtils
6
+ # Rack apparently uses 8-bit ASCII for everything, even when the string
7
+ # is not 8-bit ASCII. This reinterprets 8-bit ASCII strings as UTF-8.
8
+ #
9
+ # If we are unable to do this reinterpretation, return the string
10
+ # unchanged, but log a warning that points to the caller.
11
+ def self.fixEncoding(v)
12
+ if v != nil && v.encoding == Encoding::ASCII_8BIT then
13
+ forced = String.new(v).force_encoding(Encoding::UTF_8)
14
+ if forced.valid_encoding? then
15
+ v = forced
16
+ else
17
+ Rails.logger.warn "[#{caller_locations(1, 1)}] Unable to fix encoding: not a valid UTF-8 string. This will likely cause JSON serialization to fail."
18
+ end
19
+ end
20
+
21
+ v
22
+ end
23
+
6
24
  # Converts a Hash into a list of Hash objects. Each entry in the given
7
25
  # Hash will be represented in the output by a Hash object that maps
8
26
  # 'name' to the entry's key and 'value' to the entry's value.
9
27
  def self.hashToList(hash)
10
28
  hash.reduce([]) { |accum, (k, v)|
11
29
  accum.append({
12
- name: k,
13
- value: v,
30
+ name: fixEncoding(k),
31
+ value: fixEncoding(v),
14
32
  })
15
33
  }
16
34
  end
35
+
36
+ # Determines whether all values in a Hash are strings.
37
+ def self.allValuesAreStrings(hash)
38
+ hash.each do |_, value|
39
+ if !(value.is_a? String) then
40
+ return false
41
+ end
42
+ end
43
+
44
+ return true
45
+ end
17
46
  end
18
47
  end
19
48
  end
@@ -1,6 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'json'
4
3
  require_relative 'har_utils'
5
4
 
6
5
  module Akita
@@ -12,7 +11,7 @@ module Akita
12
11
 
13
12
  @self = {
14
13
  method: getMethod(env),
15
- url: req.url,
14
+ url: HarUtils.fixEncoding(req.url),
16
15
  httpVersion: getHttpVersion(env),
17
16
  cookies: getCookies(env),
18
17
  headers: getHeaders(env),
@@ -34,7 +33,7 @@ module Akita
34
33
 
35
34
  # Obtains the client's request method from an HTTP environment.
36
35
  def getMethod(env)
37
- (Rack::Request.new env).request_method
36
+ HarUtils.fixEncoding (Rack::Request.new env).request_method
38
37
  end
39
38
 
40
39
  # Obtains the client-requested HTTP version from an HTTP environment.
@@ -42,7 +41,9 @@ module Akita
42
41
  # The environment doesn't have HTTP_VERSION when running with `rspec`;
43
42
  # assume HTTP/1.1 when this happens. We don't return nil, so we can
44
43
  # calculate the size of the headers.
45
- env.key?('HTTP_VERSION') ? env['HTTP_VERSION'] : 'HTTP/1.1'
44
+ env.key?('HTTP_VERSION') ?
45
+ HarUtils.fixEncoding(env['HTTP_VERSION']) :
46
+ 'HTTP/1.1'
46
47
  end
47
48
 
48
49
  # Builds a list of cookie objects from an HTTP environment.
@@ -72,23 +73,71 @@ module Akita
72
73
  HarUtils.hashToList paramMap
73
74
  end
74
75
 
76
+ # Obtains the character set of the posted data from an HTTP environment.
77
+ def getPostDataCharSet(env)
78
+ req = Rack::Request.new env
79
+ if req.content_charset != nil then
80
+ return req.content_charset
81
+ end
82
+
83
+ # RFC 2616 says that "text/*" defaults to ISO-8859-1.
84
+ if env['CONTENT_TYPE'].start_with?('text/') then
85
+ return Encoding::ISO_8859_1
86
+ end
87
+
88
+ Encoding::ASCII_8BIT
89
+ end
90
+
75
91
  # Obtains the posted data from an HTTP environment.
76
92
  def getPostData(env)
77
93
  if env.key?('CONTENT_TYPE') && env['CONTENT_TYPE'] then
78
94
  result = { mimeType: env['CONTENT_TYPE'] }
79
95
 
80
96
  # Populate 'params' if we have URL-encoded parameters. Otherwise,
81
- # populate 'text.
97
+ # populate 'text'.
82
98
  req = Rack::Request.new env
83
99
  if env['CONTENT_TYPE'] == 'application/x-www-form-urlencoded' then
84
- # Decoded parameters can be found as a map in req.params. Convert
85
- # this map into an array.
100
+ # Decoded parameters can be found as a map in req.params.
86
101
  #
87
- # XXX Spec has space for files, but are file uploads ever
88
- # URL-encoded?
89
- result[:params] = HarUtils.hashToList req.params
102
+ # Requests originating from specs can be malformed: the values in
103
+ # req.params are not necessarily strings. Encode all of req.params
104
+ # in JSON and pretend the content type was "application/json".
105
+ if HarUtils.allValuesAreStrings req.params then
106
+ # Convert req.params into an array.
107
+ #
108
+ # XXX Spec has space for files, but are file uploads ever
109
+ # URL-encoded?
110
+ result[:params] = HarUtils.hashToList req.params
111
+ else
112
+ result[:mimeType] = 'application/json'
113
+ result[:text] = req.params.to_json
114
+ end
90
115
  else
91
- result[:text] = req.body.string
116
+ # Rack has been observed to use ASCII-8BIT encoding for the request
117
+ # body when the request specifies UTF-8. Reinterpret the content
118
+ # body according to what the request says it is, and re-encode into
119
+ # UTF-8.
120
+ #
121
+ # Gracefully handle any characters that are invalid in the source
122
+ # encoding and characters that have no UTF-8 representation by
123
+ # replacing with '?'. Log a warning when this happens.
124
+ source = req.body.string.force_encoding(getPostDataCharSet(env))
125
+ utf8EncodingSuccessful = false
126
+ if source.valid_encoding? then
127
+ begin
128
+ result[:text] = source.encode(Encoding::UTF_8)
129
+ utf8EncodingSuccessful = true
130
+ rescue Encoding::UndefinedConversionError
131
+ Rails.logger.warn "[#{caller_locations(0, 1)}] Unable to losslessly convert request body from #{source.encoding} to UTF-8. Characters undefined in UTF-8 will be replaced with '?'."
132
+ end
133
+ else
134
+ Rails.logger.warn "[#{caller_locations(0, 1)}] Request body is not valid #{source.encoding}. Invalid characters and characters undefined in UTF-8 will be replaced with '?'."
135
+ end
136
+
137
+ if !utf8EncodingSuccessful then
138
+ result[:text] = source.encode(Encoding::UTF_8,
139
+ invalid: :replace, undef: :replace, replace: '?')
140
+ end
92
141
  end
93
142
 
94
143
  result
@@ -1,6 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'json'
4
3
  require_relative 'har_utils'
5
4
 
6
5
  module Akita
@@ -26,7 +25,7 @@ module Akita
26
25
 
27
26
  # Obtains the status text corresponding to a status code.
28
27
  def getStatusText(status)
29
- Rack::Utils::HTTP_STATUS_CODES[status]
28
+ HarUtils.fixEncoding(Rack::Utils::HTTP_STATUS_CODES[status])
30
29
  end
31
30
 
32
31
  # Obtains the HTTP version in the response.
@@ -37,7 +36,9 @@ module Akita
37
36
  # The environment doesn't have HTTP_VERSION when running with `rspec`;
38
37
  # assume HTTP/1.1 when this happens. We don't return nil, so we can
39
38
  # calculate the size of the headers.
40
- env.key?('HTTP_VERSION') ? env['HTTP_VERSION'] : 'HTTP/1.1'
39
+ env.key?('HTTP_VERSION') ?
40
+ HarUtils.fixEncoding(env['HTTP_VERSION']) :
41
+ 'HTTP/1.1'
41
42
  end
42
43
 
43
44
  def getCookies(headers)
@@ -64,8 +65,8 @@ module Akita
64
65
  if match then cookie_value = match[1] end
65
66
 
66
67
  result << {
67
- name: cookie_name,
68
- value: cookie_value,
68
+ name: HarUtils.fixEncoding(cookie_name),
69
+ value: HarUtils.fixEncoding(cookie_value),
69
70
  }
70
71
  }
71
72
 
@@ -78,14 +79,14 @@ module Akita
78
79
  text = +""
79
80
  body.each { |part|
80
81
  # XXX Figure out how to join together multi-part bodies.
81
- text << part;
82
+ text << (HarUtils.fixEncoding part);
82
83
  }
83
84
 
84
85
  {
85
86
  size: getBodySize(body),
86
87
 
87
88
  # XXX What to use when no Content-Type is given?
88
- mimeType: headers['Content-Type'],
89
+ mimeType: HarUtils.fixEncoding(headers['Content-Type']),
89
90
 
90
91
  text: text,
91
92
  }
@@ -94,7 +95,9 @@ module Akita
94
95
  def getRedirectUrl(headers)
95
96
  # Use the "Location" header if it exists. Otherwise, based on some HAR
96
97
  # examples found online, it looks like an empty string is used.
97
- headers.key?('Location') ? headers['Location'] : ''
98
+ headers.key?('Location') ?
99
+ HarUtils.fixEncoding(headers['Location']) :
100
+ ''
98
101
  end
99
102
 
100
103
  def getHeadersSize(env, status, headers)
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Akita
4
4
  module HarLogger
5
- VERSION = "0.2.0"
5
+ VERSION = "0.2.5"
6
6
  end
7
7
  end
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'json'
4
-
5
3
  module Akita
6
4
  module HarLogger
7
5
  # A thread that consumes HarEntry objects from a queue and writes them to a
metadata CHANGED
@@ -1,29 +1,15 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: akita-har_logger
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.2.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jed Liu
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2021-04-17 00:00:00.000000000 Z
11
+ date: 2021-07-12 00:00:00.000000000 Z
12
12
  dependencies:
13
- - !ruby/object:Gem::Dependency
14
- name: json
15
- requirement: !ruby/object:Gem::Requirement
16
- requirements:
17
- - - "~>"
18
- - !ruby/object:Gem::Version
19
- version: '2.3'
20
- type: :runtime
21
- prerelease: false
22
- version_requirements: !ruby/object:Gem::Requirement
23
- requirements:
24
- - - "~>"
25
- - !ruby/object:Gem::Version
26
- version: '2.3'
27
13
  - !ruby/object:Gem::Dependency
28
14
  name: rspec
29
15
  requirement: !ruby/object:Gem::Requirement
@@ -46,6 +32,7 @@ executables: []
46
32
  extensions: []
47
33
  extra_rdoc_files: []
48
34
  files:
35
+ - ".gitignore"
49
36
  - Gemfile
50
37
  - Gemfile.lock
51
38
  - LICENSE
@@ -82,7 +69,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
82
69
  - !ruby/object:Gem::Version
83
70
  version: '0'
84
71
  requirements: []
85
- rubygems_version: 3.2.15
72
+ rubygems_version: 3.2.21
86
73
  signing_key:
87
74
  specification_version: 4
88
75
  summary: Rails middleware for HAR logging