akita-har_logger 0.1.0 → 0.2.4

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: 6c6d7e1e45f74876fef25c0840efa3b2715af8db8080411f628a7347e5d06fc9
4
- data.tar.gz: 96f8b9e4f924ec89c93847bfd293ee62b1c91700f15f76e6a5fad8f525e01ad9
3
+ metadata.gz: 51fcd2da35cb17ecb365988d7975f746a717f12b002d3a20f1d0c3eab245b10c
4
+ data.tar.gz: 4972d841dfc7c8dcd41085137d45faedeb450836d46e6d3f14d584785d997a24
5
5
  SHA512:
6
- metadata.gz: f9f73e60c027ab1756f3c64f4e1687cd5d43c13a938b971972d3ab8cb49252c9506b5127e04cc3207e9b8868b43d3d18992da755ce4ff98ca8a1a74e26e48915
7
- data.tar.gz: 5b9f0a079665e2ce5b79052ddc375843a55144e1913fb19aa423ba355234d342188a9e242f50759f28b335776f1ccf26e985459ad08d6e6a900900380d52de76
6
+ metadata.gz: 8fdffa25d122a573e8c72526934e5e713fbdc573c0d4ebfa13383d749ea856f9b01d2e5bc43e89b1c152df64f97cd965ad54bd5a4ebab924d222a1829bbbec60
7
+ data.tar.gz: 8ca06a98ce89ec4e9c3e6ac7f3b10ec98f73c44aacab1af0bac8c81649f342ec721017ee57ff6590c93a24234cce9d40c9b341aa8277a39a59bc86a3a9c3484e
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.1.0)
5
- json (~> 2.3)
4
+ akita-har_logger (0.2.4)
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
@@ -1,7 +1,7 @@
1
- # Akita HTTP Archive (HAR) logger for Rack applications
1
+ # Akita HTTP Archive (HAR) logger for Rack/Rails applications
2
2
 
3
- This provides Rack middleware for logging HTTP request–response pairs to a HAR
4
- file.
3
+ This provides Rack middleware and a Rails `ActionController` filter for logging
4
+ HTTP request–response pairs to a HAR file.
5
5
 
6
6
 
7
7
  ## Installation
@@ -23,24 +23,80 @@ Or install it yourself as:
23
23
 
24
24
  ## Usage
25
25
 
26
- To instrument your Rack application, add `Akita::HarLogger::Middleware` to the
27
- top of your middleware stack. For convenience, you can use
28
- `Akita::HarLogger.instrument`, as follows.
29
-
30
- 1. In your main `application.rb`, make `Akita::HarLogger` available:
31
- ```ruby
32
- require 'akita/har_logger'
33
- ```
34
- 2. Add the following line to the bottom of your `Rails::Application`
35
- subclass. Specifying the output file is optional; if not given, it defaults
36
- to `akita_trace_{timestamp}.har`.
37
- ```ruby
38
- Akita::HarLogger.instrument(config, '/path/to/output/har_file.har')
39
- ```
40
-
41
- Now, when you run your Rack application, all HTTP requests and responses will
42
- be logged to the HAR file that you've specified. You can then upload this HAR
43
- file to Akita for analysis.
26
+ There are two options for instrumenting your Rack/Rails application. The first
27
+ is to use the HAR logger as Rack middleware. The second is to use it as a Rails
28
+ `ActionController` filter.
29
+
30
+ Depending on the framework you're using, one or both options may be available
31
+ to you. If you are interested in logging RSpec tests, the filter option will
32
+ capture traffic for both controller and request specs, whereas the middleware
33
+ option only captures request specs.
34
+
35
+ Once your application is instrumented, when you run the application, HTTP
36
+ requests and responses will be logged to the HAR file that you've specified.
37
+ You can then upload this HAR file to Akita for analysis.
38
+
39
+ ### Middleware
40
+
41
+ To instrument with middleware, add `Akita::HarLogger::Middleware` to the top of
42
+ your middleware stack. For convenience, you can call
43
+ `Akita::HarLogger.instrument` to do this. We recommend adding this call to the
44
+ bottom of `config/environments/test.rb` to add the middleware just to your test
45
+ environment.
46
+
47
+ Here is a sample configuration for a test environment that just adds the
48
+ instrumentation.
49
+
50
+ ```ruby
51
+ # config/environments/test.rb
52
+
53
+ Rails.application.configure.do
54
+ # Other configuration for the Rails application...
55
+
56
+ # Put the HAR logger at the top of the middleware stack, and optionally
57
+ # give an output HAR file to save your trace. If not specified, this defaults
58
+ # to `akita_trace_{timestamp}.har`.
59
+ Akita::HarLogger.instrument(config, "akita_trace.har")
60
+ end
61
+ ```
62
+
63
+ ### `ActionController` filter
64
+
65
+ To instrument with a filter, add an instance of `Akita::HarLogger::Filter` as
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:
72
+
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
+
90
+ class ApplicationController < ActionController::API
91
+ include Response
92
+ include ExceptionHandler
93
+
94
+ # Add the HAR logger as an `around_action` filter. Optionally give an output
95
+ # HAR file to save your trace. If not specified, this defaults to
96
+ # `akita_trace_{timestamp}.har`.
97
+ around_action Akita::HarLogger::Filter.new("akita_trace.har") if Rails.env.test?
98
+ end
99
+ ```
44
100
 
45
101
 
46
102
  ## Development
@@ -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)
@@ -52,5 +48,72 @@ module Akita
52
48
  [ status, headers, body ]
53
49
  end
54
50
  end
51
+
52
+ # Logging filter for `ActionController`s.
53
+ # TODO: Some amount of code duplication here. Should refactor.
54
+ class Filter
55
+ def initialize(out_file_name = nil)
56
+ if out_file_name == nil then
57
+ out_file_name = HarLogger.default_file_name
58
+ end
59
+
60
+ @entry_queue = HarLogger.get_queue(out_file_name)
61
+ end
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.
72
+ def around(controller)
73
+ start_time = Time.now
74
+
75
+ yield
76
+
77
+ end_time = Time.now
78
+ wait_time_ms = ((end_time.to_f - start_time.to_f) * 1000).round
79
+
80
+ response = controller.response
81
+ request = response.request
82
+
83
+ @entry_queue << (HarEntry.new start_time, wait_time_ms, request.env,
84
+ response.status, response.headers,
85
+ [response.body])
86
+ end
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
55
118
  end
56
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,38 @@
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
+ def self.fixEncoding(v)
9
+ if v == nil || v.encoding != Encoding::ASCII_8BIT then
10
+ v
11
+ else
12
+ String.new(v).force_encoding(Encoding::UTF_8)
13
+ end
14
+ end
15
+
6
16
  # Converts a Hash into a list of Hash objects. Each entry in the given
7
17
  # Hash will be represented in the output by a Hash object that maps
8
18
  # 'name' to the entry's key and 'value' to the entry's value.
9
19
  def self.hashToList(hash)
10
20
  hash.reduce([]) { |accum, (k, v)|
11
21
  accum.append({
12
- name: k,
13
- value: v,
22
+ name: fixEncoding(k),
23
+ value: fixEncoding(v),
14
24
  })
15
25
  }
16
26
  end
27
+
28
+ # Determines whether all values in a Hash are strings.
29
+ def self.allValuesAreStrings(hash)
30
+ hash.each do |_, value|
31
+ if !(value.is_a? String) then
32
+ return false
33
+ end
34
+ end
35
+
36
+ return true
37
+ end
17
38
  end
18
39
  end
19
40
  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,52 @@ 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.default_external
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
+ result[:text] = req.body.string.encode(Encoding::UTF_8,
121
+ getPostDataCharSet(env))
92
122
  end
93
123
 
94
124
  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.1.0"
5
+ VERSION = "0.2.4"
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.1.0
4
+ version: 0.2.4
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-02 00:00:00.000000000 Z
11
+ date: 2021-07-11 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.13
72
+ rubygems_version: 3.2.21
86
73
  signing_key:
87
74
  specification_version: 4
88
75
  summary: Rails middleware for HAR logging