loginator 0.0.7 → 0.1.1

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
  SHA1:
3
- metadata.gz: 16cb733ff2fbe83f63150fd5abe6027c578a6054
4
- data.tar.gz: c8ef72fffaae51fe5cd15476216440b23e1f8afa
3
+ metadata.gz: 428aba5714b1915bcfea3a857fc67f89a155e8cc
4
+ data.tar.gz: cfb6fa8598f8a0d38a5aa82e3fd4f6c1d5910b89
5
5
  SHA512:
6
- metadata.gz: 5d5773959c66a369cffe7dea5117d2b599e73b6131f3dfea049c815e74ac5c64278754efd132b17f75dbab839f06285a24e484cf607ed5998f1a28b9c860e847
7
- data.tar.gz: ef38af19158edaec1a56c4bcbafee62303a81e0e2298d0eb288b30748a13cd4f8d74be5327f498c3c56cbc5caff4f1d62ecc457bc75b58d6fea641e36ae7b499
6
+ metadata.gz: 9762d9e7697e7a2b5a30cd9a5f6927f5a385a05363fe42bcecce74da35836a7b9e9d5638facff39d9c105e5553e6ca574904b7f724b1b4e14fcd1656f3bf1d0f
7
+ data.tar.gz: eac5555318d79d8e76c5aad08d3c9ca8107d3e2ddfdc4609572165cce517b94795b4a796067e25955fe8aeaaefbc0760061b9ebba59dff6900ee56b0e3c93b91
data/Gemfile CHANGED
@@ -1,3 +1,9 @@
1
1
  source 'https://rubygems.org'
2
2
 
3
3
  gemspec
4
+
5
+ # rubocop:disable all
6
+ # This is the jankiest jank in all of janktown. I want a better way to
7
+ # manage dependencies for middleware development.
8
+ eval(File.read('lib/loginator/middleware/Gemfile.middleware'), binding)
9
+ # rubocop:enable all
data/LICENSE.md ADDED
@@ -0,0 +1,19 @@
1
+ Copyright (c) 2015 Greg Poirier
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 CHANGED
@@ -3,8 +3,21 @@
3
3
  [![Code Climate](https://codeclimate.com/github/gray-industries/loginator/badges/gpa.svg)](https://codeclimate.com/github/gray-industries/loginator)
4
4
  [![Build Status](https://travis-ci.org/gray-industries/loginator.svg)](https://travis-ci.org/gray-industries/loginator)
5
5
 
6
- Loginator is a gem for standardizing the logging of requests and responses for
7
- remote APIs.
6
+ Loginator is a gem for standardizing different types of log formats. It particularly focuses on standardized
7
+ logging for service interactions in a SOA / distributed application.
8
+
9
+ I am tired of having useless logs at work, and I wanted to standardize those logs. Jordan Sissel puts it
10
+ much better than me:
11
+
12
+ > I want to be able to log in a structured way and have the log output know how that should be formatted.
13
+ > Maybe for humans, maybe for computers (as JSON), maybe as some other format. Maybe you want to log to a
14
+ > csv file because that's easy to load into Excel for analysis, but you don't want to change all your
15
+ > applications log methods?
16
+
17
+ I found this in the README.md for ruby-cabin (jordansissel/ruby-cabin). When I read this, I was pretty
18
+ floored. "Someone else gets it." This is exactly what I'm trying to accomplish with Loginator, but
19
+ I want to take the idea of Cabin in a slightly different direction. It's intended for use in my Gray
20
+ Industries projects, but if someone else finds it useful, that would be amazing.
8
21
 
9
22
  ## Installation
10
23
 
@@ -27,19 +40,46 @@ Or install it yourself as:
27
40
  Remote APIs (be they HTTP or otherwise) follow a pattern fairly similar to that
28
41
  of a common HTTP API.
29
42
 
30
- ### Requests
31
-
32
- A request is typically made to a path with request parameters. We attach
33
- metadata to this request in order to track it throughout a distributed system.
34
- This metadata includes a unique identifier and a UTC timestamp.
35
-
36
- ### Responses
37
-
38
- A response is typically yielded to requests. It includes the same metadata
39
- as a request, but also a response typically has associated with it a status
40
- code indicating success or failure of some kind (we have chosen to standardize
41
- around HTTP status codes -- or some interpretation thereof). Like a request,
42
- a response typically also contains a body.
43
+ ### Transactions
44
+
45
+ Transactions include the following fields:
46
+ - uuid (string)
47
+ - timestamp (serialized as a float, otherwise Time)
48
+ - duration (float)
49
+ - path (string)
50
+ - status (integer)
51
+ - request (string)
52
+ - response (string)
53
+ - params (hash)
54
+
55
+ Custom transactions can be made by extending Transaction. These custom transactions
56
+ could include additional fields, change field types, etc. In general, however, API
57
+ transactions have all or most of these characteristics regardless of protocol.
58
+
59
+ To use a transaction, wrap your API response generation in a Transaction#begin block
60
+ like so (as seen in the Sinatra middleware):
61
+
62
+ ```
63
+ Loginator::Transaction.new.begin do |txn|
64
+ txn.path = env['PATH_INFO']
65
+ txn.request = req
66
+ status, headers, body = @app.call(env)
67
+ txn.status = status
68
+ txn.response = body
69
+ [status, headers, body]
70
+ end
71
+ ```
72
+
73
+ The begin method will return the last line much like a function, allowing you
74
+ to seemlessly integrate transaction logging into your middleware.
75
+
76
+ ## Middleware
77
+
78
+ This Gem includes middleware. I am not adding explicit dependencies on the frameworks I'm targeting.
79
+ That being said, I do want to document the version of those frameworks and have put a separate Gemfile
80
+ in `lib/loginator/middleware` that includes the appropriate development dependencies required. I am
81
+ also adding that Gemfile to the gem's root Gemfile to make testing and contributing easier. I think
82
+ it is necessary to draw this distinction, because using Loginator does not explicitly require those gems.
43
83
 
44
84
  ## Contributing
45
85
 
@@ -0,0 +1 @@
1
+ gem 'sinatra', '~> 1.4.5'
@@ -0,0 +1,41 @@
1
+ require 'securerandom'
2
+
3
+ module Loginator
4
+ module Middleware
5
+ # Middleware for logging transactions with Sinatra.
6
+ class Sinatra
7
+ attr_reader :app, :logger, :transaction
8
+
9
+ # @param app [Rack::App] #{Rack::App} being passed through middleware chain
10
+ # @param logger [IO] #{IO} object where log messages will be sent via puts()
11
+ def initialize(app, logger)
12
+ @app = app
13
+ @logger = logger
14
+ end
15
+
16
+ def call(env)
17
+ uuid = env['X-REQUEST-ID'] ||= SecureRandom.uuid
18
+ req = Rack::Request.new(env)
19
+ @transaction = Loginator::Transaction.new(uuid: uuid)
20
+ transaction.begin do |txn|
21
+ txn.path = env['PATH_INFO']
22
+ txn.request = read_body(req)
23
+ status, headers, body = @app.call(env)
24
+ txn.status = status
25
+ txn.response = body
26
+ [status, headers, body]
27
+ end
28
+ ensure
29
+ logger.puts(transaction.to_json)
30
+ end
31
+
32
+ protected
33
+
34
+ def read_body(req)
35
+ req.body.read
36
+ ensure
37
+ req.body.rewind
38
+ end
39
+ end
40
+ end
41
+ end
@@ -1,21 +1,137 @@
1
1
  require 'securerandom'
2
2
 
3
3
  module Loginator
4
- # Methods for generating transactional metadata.
5
- module Transaction
6
- def self.from_json(json_str)
7
- json = MultiJson.load(json_str)
8
- Loginator.const_get(json['type'].capitalize).from_hash(json)
4
+ # A Loginator::Transaction is meant to encapsulate a single
5
+ # request/response cycle.
6
+ class Transaction
7
+ class << self
8
+ # @param [String] json JSON body of the given transaction
9
+ # @return [Transaction]
10
+ def from_json(json)
11
+ obj = MultiJson.load(json, symbolize_keys: true)
12
+ Transaction.new(filter_hash(obj))
13
+ end
14
+
15
+ protected
16
+
17
+ # This is the inverse functions for the instance-level
18
+ # filter_hash.
19
+ def filter_hash(hsh)
20
+ [[:timestamp, ->(t) { Time.at(t) }]].map do |k, f|
21
+ hsh[k] = f.call(hsh[k])
22
+ end
23
+ hsh
24
+ end
25
+ end
26
+
27
+ # The following may look funny to people, but please know that it's simply an attempt to
28
+ # convey intent. I want Transactions to be thought of as immutable objects. I'd like to
29
+ # take them in that direction eventually, but doing so in a way that is comprehensible
30
+ # is difficult. After rewriting the interface to Transaction several times, I settled here.
31
+ attr_accessor :path, :request, :params, :response, :status
32
+
33
+ attr_reader :uuid, :timestamp, :duration
34
+
35
+ protected
36
+
37
+ attr_writer :uuid, :timestamp, :duration
38
+
39
+ public
40
+
41
+ # Create a new Loginator::Transaction
42
+ # @param [Hash] opts The options to create the Loginator::Transaction
43
+ # @option opts [String] :uuid UUID (SecureRandom.uuid)
44
+ # @option opts [Time] :timestamp (Time.now) Beginning timestamp of transaction
45
+ # @option opts [Integer] :status Status returned to the requester
46
+ # @option opts [String] :path Path associated with the request
47
+ # @option opts [String] :request Body of the request
48
+ # @option opts [Hash] :params Parameters of the request
49
+ # @option opts [String] :response Body of the response
50
+ # @option opts [Float] :duration Duration of the request
51
+ def initialize(opts = {})
52
+ # TODO: UUID Generation should have a service interface
53
+ @uuid = opts.delete(:uuid) || SecureRandom.uuid
54
+ @timestamp = opts.delete(:timestamp) || Time.now
55
+ opts.each_pair do |k, v|
56
+ send("#{k}=", v)
57
+ end
58
+ end
59
+
60
+ # Marks the beginning of a Transaction. Optionally, will execute
61
+ # the given block in the context of a transaction, returning the
62
+ # last line of the block and raising any exceptions thrown in the
63
+ # block given.
64
+ #
65
+ # NOTE: we make a best effort to guarantee that `transaction.finished`
66
+ # is called via an ensure block, but that is all. We do not set the
67
+ # status or response. You must do so in your block if you wish to
68
+ # record any failures.
69
+ # @param [Block] &blk optional
70
+ # @yield [Transaction] the transaction object after it has been updated
71
+ # @return [] Returns whatever the block returns
72
+ def begin
73
+ @timestamp = Time.now
74
+ # NOTE: yield self is a bit of a smell to me, but I am okay with this
75
+ # as the block is evaluated in the context of the caller and not of
76
+ # the Transaction object.
77
+ yield self if block_given?
78
+ ensure
79
+ finished
80
+ end
81
+
82
+ # Marks the end of the transaction.
83
+ #
84
+ # NOTE: Once set, duration should be considered immutable. I.e. you
85
+ # cannot affect the duration of this Transaction by calling finished
86
+ # twice.
87
+ # @return [Time] time of the end of the transaction
88
+ def finished
89
+ fin = Time.now
90
+ @duration ||= calculate_duration(fin)
91
+ fin
92
+ end
93
+
94
+ # Hashify the Transaction
95
+ # @return [Hash]
96
+ def to_h
97
+ [:uuid, :timestamp, :status, :path, :request, :params, :response, :duration].each_with_object({}) do |key, hsh|
98
+ hsh[key] = send(key)
99
+ end
9
100
  end
10
101
 
11
- # Generate a UUID for this transaction.
12
- def uuid
13
- SecureRandom.uuid
102
+ # JSONify the Transaction
103
+ # @return [String]
104
+ def to_json
105
+ MultiJson.dump(filter_hash!(to_h))
106
+ end
107
+
108
+ # Two Transactions are considered equivalent if the following
109
+ # are all equal:
110
+ # UUID, Duration, Response, Path, Request, Parameters.
111
+ # This is largely used for testing serialization/deserialization.
112
+ def ==(other)
113
+ timestamp.to_f == other.timestamp.to_f &&
114
+ [:uuid, :duration, :response, :path, :request, :params].all? { |k| send(k) == other.send(k) }
115
+ end
116
+
117
+ protected
118
+
119
+ # @param [Time] t ending timestamp of the transaction
120
+ # @return [Float] duration of the transaction
121
+ def calculate_duration(t)
122
+ (t - timestamp).to_f
14
123
  end
15
124
 
16
- # Format the time as a float.
17
- def format_time
18
- Time.now.utc.to_f
125
+ # Filter the transaction hash's elements based on a mapping of
126
+ # element->proc pairs. This is largely for serialization/deserialization.
127
+ # NOTE: This modifies the hash in place.
128
+ # @param [Hash] hsh the hash to be modified
129
+ # @return [Hash] the modified hash
130
+ def filter_hash!(hsh)
131
+ [[:timestamp, ->(t) { t.to_f }]].each do |k, f|
132
+ hsh[k] = f.call(hsh[k])
133
+ end
134
+ hsh
19
135
  end
20
136
  end
21
137
  end
@@ -1,4 +1,4 @@
1
1
  # Increment when releasing.
2
2
  module Loginator
3
- VERSION = '0.0.7'
3
+ VERSION = '0.1.1'
4
4
  end
data/lib/loginator.rb CHANGED
@@ -1,6 +1,4 @@
1
1
  require 'loginator/version'
2
- require 'loginator/request'
3
- require 'loginator/response'
4
2
  require 'loginator/transaction'
5
3
 
6
4
  # Loginator
data/loginator.gemspec CHANGED
@@ -9,6 +9,7 @@ Gem::Specification.new do |gem|
9
9
  gem.description = 'Standardized logging of API requests/responses'
10
10
  gem.summary = 'Loginator is a mechanism for standardizing the logging of API requests and responses.'
11
11
 
12
+ gem.licenses = ['MIT']
12
13
  gem.files = `git ls-files`.split($OUTPUT_RECORD_SEPARATOR)
13
14
  gem.executables = gem.files.grep(/^bin\//).map { |f| File.basename(f) }
14
15
  gem.test_files = gem.files.grep(/^(test|spec|features)\//)
@@ -17,10 +18,6 @@ Gem::Specification.new do |gem|
17
18
  gem.version = Loginator::VERSION
18
19
 
19
20
  # dependencies...
20
- gem.add_dependency('thor', '0.19.1')
21
- gem.add_dependency('sysexits', '1.0.2')
22
- gem.add_dependency('awesome_print', '~> 1.1.0')
23
- gem.add_dependency('abstract_type', '~> 0.0.7')
24
21
  gem.add_dependency('multi_json', '~> 1.10.1')
25
22
 
26
23
  # development dependencies.
@@ -0,0 +1,60 @@
1
+ require 'loginator/middleware/sinatra'
2
+ require 'rack'
3
+
4
+ describe Loginator::Middleware::Sinatra do
5
+ let(:app) { ->(_env) { response } }
6
+ let(:response) { [200, env, 'app'] }
7
+ let(:env) { Rack::MockRequest.env_for('http://test/path', input: body) }
8
+ let(:body) { StringIO.new('Request body') }
9
+ let(:logger) { StringIO.new }
10
+ let(:transaction) { subject.transaction }
11
+
12
+ subject { described_class.new(app, logger) }
13
+
14
+ context 'app call succeeds' do
15
+ before do
16
+ subject.call(env)
17
+ end
18
+
19
+ it 'adds the UUID to the sinatra env' do
20
+ expect(transaction.uuid).to be_a_kind_of(String)
21
+ end
22
+
23
+ it 'rewinds the Sinatra request body' do
24
+ expect(body.eof?).to be_falsey
25
+ end
26
+
27
+ it 'sets the request path' do
28
+ expect(transaction.path).to eq('/path')
29
+ end
30
+
31
+ it 'sets the request body' do
32
+ expect(transaction.request).to eq(body.string)
33
+ end
34
+
35
+ it 'sets the timestamp' do
36
+ expect(transaction.timestamp).to be_a_kind_of(Time)
37
+ end
38
+
39
+ it 'sets the response code' do
40
+ expect(transaction.status).to eq(200)
41
+ end
42
+
43
+ it 'sets the response body' do
44
+ expect(transaction.response).to eq('app')
45
+ end
46
+
47
+ it 'logs the transaction' do
48
+ expect(logger.string.chomp).to eq(transaction.to_json)
49
+ end
50
+ end
51
+
52
+ context 'app call fails' do
53
+ let(:app) { ->(_env) { fail } }
54
+ before { expect { subject.call(env) }.to raise_error }
55
+
56
+ it 'still logs the message' do
57
+ expect(logger.string.chomp).to eq(transaction.to_json)
58
+ end
59
+ end
60
+ end
@@ -1,23 +1,139 @@
1
1
  require 'loginator/transaction'
2
2
 
3
3
  RSpec.describe Loginator::Transaction do
4
- subject { Loginator.const_get(type).new }
4
+ describe '#new' do
5
+ before do
6
+ subject.finished
7
+ end
5
8
 
6
- describe '.from_json' do
7
- context 'when passed a request json body' do
8
- let(:type) { 'Request' }
9
+ context 'using defaults' do
10
+ it 'creates a valid object' do
11
+ expect(subject).to be_a_kind_of(described_class)
12
+ end
13
+
14
+ it 'sets default request id' do
15
+ expect(subject.uuid).to be_a_kind_of(String)
16
+ end
17
+
18
+ it 'sets default timestamp' do
19
+ expect(subject.timestamp).to be_a_kind_of(Time)
20
+ expect(subject.timestamp).to be <= Time.now
21
+ end
22
+ end
23
+
24
+ context 'overriding defaults' do
25
+ let(:uuid) { 'uuid' }
26
+ let(:timestamp) { Time.at(42) }
27
+ let(:path) { '/' }
28
+
29
+ subject { described_class.new(uuid: uuid, timestamp: timestamp, duration: 0, path: path) }
30
+
31
+ [:uuid, :timestamp, :path].each do |field|
32
+ it 'overrides defaults' do
33
+ expect(subject.send(field)).to eq(send(field))
34
+ end
35
+ end
9
36
 
10
- it 'returns a request' do
11
- expect(Loginator::Transaction.from_json(subject.to_json)).to be_a_kind_of(Loginator::Request)
37
+ describe '.from_json' do
38
+ it 'faithfully deserializes' do
39
+ expect(described_class.from_json(subject.to_json)).to eq(subject)
40
+ end
12
41
  end
13
42
  end
43
+ end
44
+
45
+ describe '#finished' do
46
+ before do
47
+ subject.finished
48
+ end
49
+
50
+ it 'sets a positive float for the duration' do
51
+ expect(subject.duration).to be_a_kind_of(Float)
52
+ expect(subject.duration).to be > 0.0
53
+ end
54
+ end
14
55
 
15
- context 'when passed a response json body' do
16
- let(:type) { 'Response' }
56
+ describe '#begin' do
57
+ shared_examples_for 'transaction#begin' do
58
+ it 'marks the beginning of the transaction' do
59
+ expect(subject.timestamp).to be_a_kind_of(Time)
60
+ end
61
+
62
+ it 'executes the block' do
63
+ expect(@in_block.to_f).to be > subject.timestamp.to_f
64
+ end
17
65
 
18
- it 'recognizes a response' do
19
- expect(Loginator::Transaction.from_json(subject.to_json)).to be_a_kind_of(Loginator::Response)
66
+ it 'marks the end of the transaction after the block is finished' do
67
+ expect((subject.timestamp + subject.duration).to_f).to be > @in_block.to_f
20
68
  end
21
69
  end
70
+
71
+ context 'when the block executes normally' do
72
+ before do
73
+ subject.begin do
74
+ sleep 0.01
75
+ @in_block = Time.now
76
+ end
77
+
78
+ it_behaves_like 'transaction#begin'
79
+ end
80
+ end
81
+
82
+ context 'when the block passed raises an exception' do
83
+ before do
84
+ expect do
85
+ subject.begin do
86
+ sleep 0.01
87
+ @in_block = Time.now
88
+ fail
89
+ end
90
+ end.to raise_error
91
+ end
92
+
93
+ it_behaves_like 'transaction#begin'
94
+ end
95
+ end
96
+
97
+ describe '#to_json' do
98
+ let(:json) { MultiJson.dump(hash) }
99
+
100
+ let(:hash) do
101
+ {
102
+ 'uuid' => '1',
103
+ 'timestamp' => 0.0,
104
+ 'duration' => 1.0,
105
+ 'path' => '/',
106
+ 'status' => 0,
107
+ 'request' => '',
108
+ 'response' => '',
109
+ 'params' => {}
110
+ }
111
+ end
112
+
113
+ subject { described_class.from_json(json) }
114
+
115
+ before do
116
+ subject.finished
117
+ end
118
+
119
+ it 'faithfully serializes' do
120
+ %w(uuid duration path status request response params).each do |k|
121
+ expect(subject.send(k)).to eq(hash[k])
122
+ end
123
+
124
+ expect(subject.timestamp.to_f).to eq(hash['timestamp'])
125
+ end
126
+ end
127
+
128
+ describe '.from_json' do
129
+ subject { described_class.new(duration: 1.0, timestamp: Time.now) }
130
+
131
+ before do
132
+ subject.finished
133
+ end
134
+
135
+ it 'faithfully deserializes' do
136
+ expect(described_class.from_json(subject.to_json)).to eq(subject)
137
+ end
22
138
  end
23
139
  end
metadata CHANGED
@@ -1,71 +1,15 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: loginator
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.7
4
+ version: 0.1.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Greg Poirier
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2015-02-14 00:00:00.000000000 Z
11
+ date: 2015-03-09 00:00:00.000000000 Z
12
12
  dependencies:
13
- - !ruby/object:Gem::Dependency
14
- name: thor
15
- requirement: !ruby/object:Gem::Requirement
16
- requirements:
17
- - - '='
18
- - !ruby/object:Gem::Version
19
- version: 0.19.1
20
- type: :runtime
21
- prerelease: false
22
- version_requirements: !ruby/object:Gem::Requirement
23
- requirements:
24
- - - '='
25
- - !ruby/object:Gem::Version
26
- version: 0.19.1
27
- - !ruby/object:Gem::Dependency
28
- name: sysexits
29
- requirement: !ruby/object:Gem::Requirement
30
- requirements:
31
- - - '='
32
- - !ruby/object:Gem::Version
33
- version: 1.0.2
34
- type: :runtime
35
- prerelease: false
36
- version_requirements: !ruby/object:Gem::Requirement
37
- requirements:
38
- - - '='
39
- - !ruby/object:Gem::Version
40
- version: 1.0.2
41
- - !ruby/object:Gem::Dependency
42
- name: awesome_print
43
- requirement: !ruby/object:Gem::Requirement
44
- requirements:
45
- - - "~>"
46
- - !ruby/object:Gem::Version
47
- version: 1.1.0
48
- type: :runtime
49
- prerelease: false
50
- version_requirements: !ruby/object:Gem::Requirement
51
- requirements:
52
- - - "~>"
53
- - !ruby/object:Gem::Version
54
- version: 1.1.0
55
- - !ruby/object:Gem::Dependency
56
- name: abstract_type
57
- requirement: !ruby/object:Gem::Requirement
58
- requirements:
59
- - - "~>"
60
- - !ruby/object:Gem::Version
61
- version: 0.0.7
62
- type: :runtime
63
- prerelease: false
64
- version_requirements: !ruby/object:Gem::Requirement
65
- requirements:
66
- - - "~>"
67
- - !ruby/object:Gem::Version
68
- version: 0.0.7
69
13
  - !ruby/object:Gem::Dependency
70
14
  name: multi_json
71
15
  requirement: !ruby/object:Gem::Requirement
@@ -262,14 +206,14 @@ files:
262
206
  - ".travis.yml"
263
207
  - Gemfile
264
208
  - Guardfile
209
+ - LICENSE.md
265
210
  - README.md
266
211
  - Rakefile
267
212
  - examples/rack/logging.rb
268
213
  - examples/rack/uuid.rb
269
214
  - lib/loginator.rb
270
- - lib/loginator/jsonable_struct.rb
271
- - lib/loginator/request.rb
272
- - lib/loginator/response.rb
215
+ - lib/loginator/middleware/Gemfile.middleware
216
+ - lib/loginator/middleware/sinatra.rb
273
217
  - lib/loginator/transaction.rb
274
218
  - lib/loginator/version.rb
275
219
  - loginator.gemspec
@@ -277,17 +221,15 @@ files:
277
221
  - spec/fixtures.rb
278
222
  - spec/fixtures/.gitignore
279
223
  - spec/integration/lib/loginator/.gitignore
280
- - spec/integration/lib/loginator/request_spec.rb
281
- - spec/integration/lib/loginator/response_spec.rb
224
+ - spec/integration/lib/loginator/middleware/sinatra_spec.rb
282
225
  - spec/integration/lib/loginator/transaction_spec.rb
283
226
  - spec/resources.rb
284
227
  - spec/resources/.gitignore
285
228
  - spec/spec_helper.rb
286
- - spec/support/shared/lib/loginator/jsonable_struct_spec.rb
287
- - spec/support/shared/lib/loginator/struct_with_defaults_spec.rb
288
229
  - spec/unit/lib/loginator/.gitignore
289
230
  homepage:
290
- licenses: []
231
+ licenses:
232
+ - MIT
291
233
  metadata: {}
292
234
  post_install_message:
293
235
  rdoc_options: []
@@ -315,13 +257,10 @@ test_files:
315
257
  - spec/fixtures.rb
316
258
  - spec/fixtures/.gitignore
317
259
  - spec/integration/lib/loginator/.gitignore
318
- - spec/integration/lib/loginator/request_spec.rb
319
- - spec/integration/lib/loginator/response_spec.rb
260
+ - spec/integration/lib/loginator/middleware/sinatra_spec.rb
320
261
  - spec/integration/lib/loginator/transaction_spec.rb
321
262
  - spec/resources.rb
322
263
  - spec/resources/.gitignore
323
264
  - spec/spec_helper.rb
324
- - spec/support/shared/lib/loginator/jsonable_struct_spec.rb
325
- - spec/support/shared/lib/loginator/struct_with_defaults_spec.rb
326
265
  - spec/unit/lib/loginator/.gitignore
327
266
  has_rdoc:
@@ -1,45 +0,0 @@
1
- require 'multi_json'
2
-
3
- module Loginator
4
- # Makes a Struct easily serializable and deserializable. Adds the
5
- # from_json class method and to_json instance method to Struct
6
- # classes.
7
- module JsonableStruct
8
- def self.included(base)
9
- base.extend ClassMethods
10
- end
11
-
12
- # class level mixins
13
- module ClassMethods #:nodoc
14
- def from_hash(hsh)
15
- hsh_type = hsh.delete('type')
16
- fail(ArgumentError, format('Incorrect message type: %s', hsh_type)) unless hsh_type == type
17
- fail(ArgumentError, format('Hash must contain keys: %s', members.join(', '))) unless valid_hash?(hsh)
18
- new(*hsh.values)
19
- end
20
-
21
- def from_json(json_str)
22
- json = MultiJson.load(json_str)
23
- from_hash(json)
24
- end
25
-
26
- def type
27
- @type ||= name.split('::').last.downcase.freeze
28
- end
29
-
30
- private
31
-
32
- def valid_hash?(hsh)
33
- # Loosely validate that all necessary keys are present...
34
- # This does mean that we could potentially create a Response from a hash
35
- # representing an object _like_ a response.... Feature or... ? ^_^
36
- members & hsh.keys.map(&:to_sym) == members
37
- end
38
- end #:rubocop:enable documentation
39
-
40
- def to_json
41
- MultiJson.dump(to_h.merge(type: self.class.type))
42
- end
43
- alias_method :to_s, :to_json
44
- end
45
- end
@@ -1,25 +0,0 @@
1
- require 'multi_json'
2
- require 'loginator/jsonable_struct'
3
- require 'loginator/transaction'
4
-
5
- # Loginator::Request
6
- module Loginator
7
- # A request is a tuple of a (UUID, Timestamp, Path, Parameters). Requests parameters are defined subjectively by the
8
- # user. For example, in the example Rack middleware ({#Rack::Loginator::Logging}), we define params as the request
9
- # body (our HTTP APIs tend to accept JSON bodies as opposed to parameters attached to the URL). You may also wish
10
- # to consider part of the HTTP headers as a request parameter.
11
- #
12
- Request = Struct.new(:request_id, :timestamp, :path, :params) do
13
- include Loginator::JsonableStruct
14
- include Loginator::Transaction
15
-
16
- # Create a new Loginator::Request
17
- # @param request_id [String] (SecureRandom.uuid) Unique identifier for the request
18
- # @param timestamp [Float] (Time.now.utc.to_f) Time of the request
19
- # @param path [String] (nil) Path associated with the request
20
- # @param params [String] ({}) Parameters of the request
21
- def initialize(request_id = uuid, timestamp = format_time, path = nil, params = {})
22
- super
23
- end
24
- end
25
- end
@@ -1,27 +0,0 @@
1
- require 'multi_json'
2
- require 'loginator/jsonable_struct'
3
- require 'loginator/transaction'
4
-
5
- # Loginator::Response
6
- module Loginator
7
- # A {#Loginator::Response} is a response to a {#Loginator::Request}. It should include the same elements as a
8
- # request, plus the status of the response (an indicator if the API request was successful or not) as well
9
- # as an optional response body. Whether or not to log the response is entirely left up to implementation
10
- # decisions and production log volume considerations. It is trivial to log response bodies in development,
11
- # but not in production.
12
- #
13
- Response = Struct.new(:request_id, :timestamp, :path, :status, :body) do
14
- include Loginator::JsonableStruct
15
- include Loginator::Transaction
16
-
17
- # Create a new Loginator::Response
18
- # @param request_id [String] (SecureRandom.uuid) Unique identifier for the request
19
- # @param timestamp [Float] (Time.now.utc.to_f) Time of the request
20
- # @param path [String] (nil) Path associated with the request
21
- # @param status [Integer] (0) Status returned to the requester
22
- # @param body [String] ({}) Parameters of the request
23
- def initialize(request_id = uuid, timestamp = format_time, path = nil, status = 0, body = '')
24
- super
25
- end
26
- end
27
- end
@@ -1,13 +0,0 @@
1
- require 'loginator/request'
2
- require 'support/shared/lib/loginator/jsonable_struct_spec'
3
- require 'support/shared/lib/loginator/struct_with_defaults_spec'
4
-
5
- RSpec.describe Loginator::Request do
6
- describe '#new' do
7
- it_behaves_like 'struct_with_defaults'
8
- end
9
-
10
- describe 'serializability' do
11
- it_behaves_like 'jsonable_struct'
12
- end
13
- end
@@ -1,13 +0,0 @@
1
- require 'loginator/response'
2
- require 'support/shared/lib/loginator/jsonable_struct_spec'
3
- require 'support/shared/lib/loginator/struct_with_defaults_spec'
4
-
5
- RSpec.describe Loginator::Response do
6
- describe '#new' do
7
- it_behaves_like 'struct_with_defaults'
8
- end
9
-
10
- describe 'serializability' do
11
- it_behaves_like 'jsonable_struct'
12
- end
13
- end
@@ -1,13 +0,0 @@
1
- shared_examples_for 'jsonable_struct' do
2
- describe '#to_json' do
3
- it 'faithfully serializes' do
4
- expect(subject.to_json).to eq(subject.to_h.merge(type: described_class.type).to_json)
5
- end
6
- end
7
-
8
- describe '.from_json' do
9
- it 'faithfully deserializes' do
10
- expect(described_class.from_json(subject.to_json)).to eq(subject)
11
- end
12
- end
13
- end
@@ -1,39 +0,0 @@
1
- shared_examples_for 'struct_with_defaults' do
2
- describe '#new' do
3
- context 'using defaults' do
4
- it 'creates a valid object' do
5
- expect(subject).to be_a_kind_of(described_class)
6
- end
7
-
8
- it 'sets default request id' do
9
- expect(subject.request_id).to be_a_kind_of(String)
10
- end
11
-
12
- it 'sets default timestamp' do
13
- expect(subject.timestamp).to be_a_kind_of(Float)
14
- expect(subject.timestamp).to be <= Time.now.utc.to_f
15
- expect(subject.timestamp).to be > 0.0
16
- end
17
- end
18
-
19
- context 'overriding defaults' do
20
- let(:request_id) { 'uuid' }
21
- let(:timestamp) { 42 }
22
- let(:path) { '/' }
23
-
24
- subject { described_class.new(request_id, timestamp, path) }
25
-
26
- [:request_id, :timestamp, :path].each do |field|
27
- it 'overrides defaults' do
28
- expect(subject.send(field)).to eq(send(field))
29
- end
30
- end
31
-
32
- describe '.from_json' do
33
- it 'faithfully deserializes' do
34
- expect(described_class.from_json(subject.to_json)).to eq(subject)
35
- end
36
- end
37
- end
38
- end
39
- end