loginator 0.0.7 → 0.1.1

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
  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