service_template 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (105) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +21 -0
  3. data/.rubocop.yml +23 -0
  4. data/.travis.yml +13 -0
  5. data/CHANGELOG.md +64 -0
  6. data/Gemfile +4 -0
  7. data/LICENSE +24 -0
  8. data/README.md +217 -0
  9. data/Rakefile +9 -0
  10. data/bin/service_template +5 -0
  11. data/lib/service_template.rb +55 -0
  12. data/lib/service_template/active_record_extensions/notifications_subscriber.rb +17 -0
  13. data/lib/service_template/active_record_extensions/seeder.rb +14 -0
  14. data/lib/service_template/active_record_extensions/stats.rb +37 -0
  15. data/lib/service_template/authentication.rb +8 -0
  16. data/lib/service_template/cli.rb +111 -0
  17. data/lib/service_template/deploy.rb +98 -0
  18. data/lib/service_template/gem_dependency.rb +37 -0
  19. data/lib/service_template/generators.rb +3 -0
  20. data/lib/service_template/generators/api_generator.rb +30 -0
  21. data/lib/service_template/generators/readme_generator.rb +47 -0
  22. data/lib/service_template/generators/scaffold_generator.rb +29 -0
  23. data/lib/service_template/generators/templates/api/app/apis/%name_tableize%_api.rb.tt +40 -0
  24. data/lib/service_template/generators/templates/api/app/models/%name_underscore%.rb.tt +2 -0
  25. data/lib/service_template/generators/templates/api/app/representers/%name_underscore%_representer.rb.tt +4 -0
  26. data/lib/service_template/generators/templates/api/spec/apis/%name_tableize%_api_spec.rb.tt +16 -0
  27. data/lib/service_template/generators/templates/api/spec/models/%name_underscore%_spec.rb.tt +9 -0
  28. data/lib/service_template/generators/templates/readme/README.md.tt +55 -0
  29. data/lib/service_template/generators/templates/readme/spec/docs/readme_spec.rb +7 -0
  30. data/lib/service_template/generators/templates/scaffold/.env.development.tt +9 -0
  31. data/lib/service_template/generators/templates/scaffold/.env.test.tt +10 -0
  32. data/lib/service_template/generators/templates/scaffold/.gitignore.tt +13 -0
  33. data/lib/service_template/generators/templates/scaffold/.rubocop.yml +24 -0
  34. data/lib/service_template/generators/templates/scaffold/.ruby-version.tt +1 -0
  35. data/lib/service_template/generators/templates/scaffold/Gemfile.tt +29 -0
  36. data/lib/service_template/generators/templates/scaffold/README.md +3 -0
  37. data/lib/service_template/generators/templates/scaffold/Rakefile +21 -0
  38. data/lib/service_template/generators/templates/scaffold/app.rb +19 -0
  39. data/lib/service_template/generators/templates/scaffold/app/apis/application_api.rb +9 -0
  40. data/lib/service_template/generators/templates/scaffold/app/apis/hello_api.rb.tt +10 -0
  41. data/lib/service_template/generators/templates/scaffold/config.ru.tt +21 -0
  42. data/lib/service_template/generators/templates/scaffold/config/database.yml.tt +19 -0
  43. data/lib/service_template/generators/templates/scaffold/config/initializers/active_record.rb +5 -0
  44. data/lib/service_template/generators/templates/scaffold/db/schema.rb +11 -0
  45. data/lib/service_template/generators/templates/scaffold/lib/.keep +0 -0
  46. data/lib/service_template/generators/templates/scaffold/log/.keep +0 -0
  47. data/lib/service_template/generators/templates/scaffold/spec/apis/hello_api_spec.rb.tt +17 -0
  48. data/lib/service_template/generators/templates/scaffold/spec/factories/.gitkeep +0 -0
  49. data/lib/service_template/generators/templates/scaffold/spec/spec_helper.rb +47 -0
  50. data/lib/service_template/grape_extenders.rb +30 -0
  51. data/lib/service_template/grape_extensions/error_formatter.rb +18 -0
  52. data/lib/service_template/grape_extensions/grape_helpers.rb +27 -0
  53. data/lib/service_template/identity.rb +45 -0
  54. data/lib/service_template/json_error.rb +24 -0
  55. data/lib/service_template/logger/log_transaction.rb +17 -0
  56. data/lib/service_template/logger/logger.rb +42 -0
  57. data/lib/service_template/logger/parseable.rb +37 -0
  58. data/lib/service_template/middleware/app_monitor.rb +17 -0
  59. data/lib/service_template/middleware/authentication.rb +32 -0
  60. data/lib/service_template/middleware/database_stats.rb +15 -0
  61. data/lib/service_template/middleware/logger.rb +67 -0
  62. data/lib/service_template/middleware/request_stats.rb +42 -0
  63. data/lib/service_template/output_formatters/entity.rb +15 -0
  64. data/lib/service_template/output_formatters/include_nil.rb +16 -0
  65. data/lib/service_template/output_formatters/json_api_representer.rb +9 -0
  66. data/lib/service_template/param_sanitizer.rb +30 -0
  67. data/lib/service_template/rspec_extensions/response_helpers.rb +46 -0
  68. data/lib/service_template/setup.rb +36 -0
  69. data/lib/service_template/sortable_api.rb +17 -0
  70. data/lib/service_template/stats.rb +43 -0
  71. data/lib/service_template/stats_d_timer.rb +26 -0
  72. data/lib/service_template/version.rb +45 -0
  73. data/lib/tasks/deploy.rake +11 -0
  74. data/lib/tasks/routes.rake +11 -0
  75. data/service_template.gemspec +42 -0
  76. data/spec/active_record_extensions/filter_by_hash_spec.rb +23 -0
  77. data/spec/active_record_extensions/seeder_spec.rb +13 -0
  78. data/spec/authentication_spec.rb +17 -0
  79. data/spec/deprecations/application_api_spec.rb +19 -0
  80. data/spec/deprecations/entity_spec.rb +9 -0
  81. data/spec/deprecations/filter_by_hash_spec.rb +9 -0
  82. data/spec/deprecations/napa_setup_spec.rb +52 -0
  83. data/spec/generators/api_generator_spec.rb +63 -0
  84. data/spec/generators/migration_generator_spec.rb +105 -0
  85. data/spec/generators/readme_generator_spec.rb +35 -0
  86. data/spec/generators/scaffold_generator_spec.rb +90 -0
  87. data/spec/grape_extenders_spec.rb +50 -0
  88. data/spec/grape_extensions/error_formatter_spec.rb +29 -0
  89. data/spec/grape_extensions/include_nil_spec.rb +23 -0
  90. data/spec/identity_spec.rb +50 -0
  91. data/spec/json_error_spec.rb +33 -0
  92. data/spec/logger/log_transaction_spec.rb +34 -0
  93. data/spec/logger/logger_spec.rb +14 -0
  94. data/spec/logger/parseable_spec.rb +16 -0
  95. data/spec/middleware/authentication_spec.rb +54 -0
  96. data/spec/middleware/database_stats_spec.rb +64 -0
  97. data/spec/middleware/request_stats_spec.rb +21 -0
  98. data/spec/sortable_api_spec.rb +56 -0
  99. data/spec/spec_helper.rb +45 -0
  100. data/spec/stats_d_timer_spec.rb +23 -0
  101. data/spec/stats_spec.rb +66 -0
  102. data/spec/version_spec.rb +40 -0
  103. data/tasks/spec.rake +9 -0
  104. data/tasks/version.rake +51 -0
  105. metadata +456 -0
@@ -0,0 +1,27 @@
1
+ module ServiceTemplate
2
+ module GrapeHelpers
3
+ def represent(data, with: nil, **args)
4
+ raise ArgumentError.new(":with option is required") if with.nil?
5
+
6
+ if data.respond_to?(:to_a)
7
+ return { data: data.map{ |item| with.new(item).to_hash(args) } }
8
+ else
9
+ return { data: with.new(data).to_hash(args)}
10
+ end
11
+ end
12
+
13
+ def present_error(code, message = '', reasons={})
14
+ ServiceTemplate::JsonError.new(code, message, reasons)
15
+ end
16
+
17
+ def permitted_params(options = {})
18
+ options = { include_missing: false }.merge(options)
19
+ declared(params, options)
20
+ end
21
+
22
+ # extend all endpoints to include this
23
+ Grape::Endpoint.send :include, self if defined?(Grape)
24
+ # rails 4 controller concern
25
+ extend ActiveSupport::Concern if defined?(Rails)
26
+ end
27
+ end
@@ -0,0 +1,45 @@
1
+ module ServiceTemplate
2
+ class Identity
3
+ def self.health
4
+ {
5
+ name: name,
6
+ hostname: hostname,
7
+ revision: revision,
8
+ pid: pid,
9
+ parent_pid: parent_pid,
10
+ platform: platform
11
+ }
12
+ end
13
+
14
+ def self.name
15
+ ENV['SERVICE_NAME'] || 'api-service'
16
+ end
17
+
18
+ def self.hostname
19
+ @hostname ||= `hostname`.strip
20
+ end
21
+
22
+ def self.revision
23
+ @revision ||= `git rev-parse HEAD`.strip
24
+ end
25
+
26
+ def self.pid
27
+ @pid ||= Process.pid
28
+ end
29
+
30
+ def self.parent_pid
31
+ @ppid ||= Process.ppid
32
+ end
33
+
34
+ def self.platform
35
+ {
36
+ version: platform_revision,
37
+ name: "ServiceTemplate"
38
+ }
39
+ end
40
+
41
+ def self.platform_revision
42
+ ServiceTemplate::VERSION
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,24 @@
1
+ module ServiceTemplate
2
+ class JsonError
3
+ def initialize(code, message, reasons={})
4
+ @code = code
5
+ @message = message
6
+ @reasons = reasons
7
+ end
8
+
9
+ def to_json(options = {})
10
+ to_h.to_json(options)
11
+ end
12
+
13
+ def to_h
14
+ e = {
15
+ error: {
16
+ code: @code,
17
+ message: @message
18
+ }
19
+ }
20
+ e[:error][:reasons] = @reasons if @reasons.present?
21
+ e
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,17 @@
1
+ module ServiceTemplate
2
+ class LogTransaction
3
+ class << self
4
+ def id
5
+ Thread.current[:service_template_tid].nil? ? Thread.current[:service_template_tid] = SecureRandom.hex(10) : Thread.current[:service_template_tid]
6
+ end
7
+
8
+ def id=(id)
9
+ Thread.current[:service_template_tid] = id
10
+ end
11
+
12
+ def clear
13
+ Thread.current[:service_template_tid] = nil
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,42 @@
1
+ module ServiceTemplate
2
+ class Logger
3
+ class << self
4
+ def name
5
+ [ServiceTemplate::Identity.name, ServiceTemplate::LogTransaction.id].join('-')
6
+ end
7
+
8
+ def logger=(logger)
9
+ @logger = logger
10
+ end
11
+
12
+ def logger
13
+ unless @logger
14
+ Logging.appenders.stdout(
15
+ 'stdout',
16
+ layout: Logging.layouts.json
17
+ )
18
+ Logging.appenders.file(
19
+ "log/#{ServiceTemplate.env}.log",
20
+ layout: Logging.layouts.json
21
+ )
22
+
23
+ @logger = Logging.logger["[#{name}]"]
24
+ @logger.add_appenders 'stdout' unless ServiceTemplate.env.test?
25
+ @logger.add_appenders "log/#{ServiceTemplate.env}.log"
26
+ end
27
+
28
+ @logger
29
+ end
30
+
31
+ def response(status, headers, body)
32
+ { response:
33
+ {
34
+ status: status,
35
+ headers: headers,
36
+ response: body
37
+ }
38
+ }
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,37 @@
1
+ # override what is in logging gem to ALWAYS use a structured object, rather than a string
2
+ # original version of this: https://github.com/TwP/logging/blob/master/lib/logging/layouts/parseable.rb
3
+ module Logging
4
+ module Layouts
5
+ class Parseable < ::Logging::Layout
6
+
7
+ # Public: Take a given object and convert it into a format suitable for
8
+ # inclusion as a log message. The conversion allows the object to be more
9
+ # easily expressed in YAML or JSON form.
10
+ #
11
+ # If the object is an Exception, then this method will return a Hash
12
+ # containing the exception class name, message, and backtrace (if any).
13
+ #
14
+ # If the object is a string, wrap it in a hash.
15
+ #
16
+ # obj - The Object to format
17
+ #
18
+ # Returns the formatted Object.
19
+ #
20
+ def format_obj(obj)
21
+ case obj
22
+ when Exception
23
+ h = { class: obj.class.name,
24
+ message: obj.message }
25
+ h[:backtrace] = obj.backtrace if @backtrace && !obj.backtrace.nil?
26
+ h
27
+ when Time
28
+ iso8601_format(obj)
29
+ when String
30
+ { text: obj }
31
+ else
32
+ obj
33
+ end
34
+ end
35
+ end # Parseable
36
+ end # Layouts
37
+ end
@@ -0,0 +1,17 @@
1
+ module ServiceTemplate
2
+ class Middleware
3
+ class AppMonitor
4
+ def initialize(app)
5
+ @app = app
6
+ end
7
+
8
+ def call(env)
9
+ if ["/health", "/health.json"].include? env['PATH_INFO']
10
+ [200, { 'Content-type' => 'application/json' }, [ServiceTemplate::Identity.health.to_json]]
11
+ else
12
+ @app.call(env)
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,32 @@
1
+ module ServiceTemplate
2
+ class Middleware
3
+ class Authentication
4
+ def initialize(app)
5
+ @app = app
6
+
7
+ if ENV['HEADER_PASSWORDS']
8
+ @allowed_passwords = ENV['HEADER_PASSWORDS'].split(',').map { |pw| pw.strip }.freeze
9
+ end
10
+ end
11
+
12
+ def call(env)
13
+ if authenticated_request?(env)
14
+ @app.call(env)
15
+ else
16
+ if @allowed_passwords
17
+ error_response = ServiceTemplate::JsonError.new('bad_password', 'bad password').to_json
18
+ else
19
+ error_response = ServiceTemplate::JsonError.new('not_configured', 'password not configured').to_json
20
+ end
21
+
22
+ [401, { 'Content-type' => 'application/json' }, Array.wrap(error_response)]
23
+ end
24
+
25
+ end
26
+
27
+ def authenticated_request?(env)
28
+ @allowed_passwords.include? env['HTTP_PASSWORD'] unless @allowed_passwords.nil?
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,15 @@
1
+ module ServiceTemplate
2
+ class Middleware
3
+ class DatabaseStats
4
+ def initialize(app)
5
+ @app = app
6
+ end
7
+
8
+ def call(env)
9
+ require 'service_template/active_record_extensions/notifications_subscriber'
10
+ status, headers, body = @app.call(env)
11
+ [status, headers, body]
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,67 @@
1
+ require 'service_template/param_sanitizer'
2
+
3
+ module ServiceTemplate
4
+ class Middleware
5
+ class Logger
6
+ include ServiceTemplate::ParamSanitizer
7
+
8
+ def initialize(app)
9
+ @app = app
10
+ end
11
+
12
+ def call(env)
13
+ # log the request
14
+ ServiceTemplate::Logger.logger.info format_request(env)
15
+
16
+ # process the request
17
+ status, headers, body = @app.call(env)
18
+
19
+ # log the response
20
+ ServiceTemplate::Logger.logger.debug format_response(status, headers, body)
21
+
22
+ # return the results
23
+ [status, headers, body]
24
+ ensure
25
+ # Clear the transaction id after each request
26
+ ServiceTemplate::LogTransaction.clear
27
+ end
28
+
29
+ private
30
+
31
+ def format_request(env)
32
+ request = Rack::Request.new(env)
33
+ params = request.params
34
+
35
+ begin
36
+ params = JSON.parse(request.body.read) if env['CONTENT_TYPE'] == 'application/json'
37
+ rescue
38
+ # do nothing, params is already set
39
+ end
40
+
41
+ request_data = {
42
+ method: request.request_method,
43
+ path: request.path_info,
44
+ query: filtered_query_string(request.query_string),
45
+ host: ServiceTemplate::Identity.hostname,
46
+ pid: ServiceTemplate::Identity.pid,
47
+ revision: ServiceTemplate::Identity.revision,
48
+ params: filtered_parameters(params),
49
+ remote_ip: request.ip
50
+ }
51
+ request_data[:user_id] = current_user.try(:id) if defined?(current_user)
52
+ { request: request_data }
53
+ end
54
+
55
+ def format_response(status, headers, body)
56
+ response_body = nil
57
+ begin
58
+ response_body = body.respond_to?(:body) ? body.body.map { |r| r } : nil
59
+ rescue
60
+ response_body = body.inspect
61
+ end
62
+
63
+ ServiceTemplate::Logger.response(status, headers, response_body)
64
+ end
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,42 @@
1
+ module ServiceTemplate
2
+ class Middleware
3
+ class RequestStats
4
+ def initialize(app)
5
+ @app = app
6
+ end
7
+
8
+ def normalize_path(path)
9
+ case
10
+ when path == '/'
11
+ 'root'
12
+ else
13
+ path.start_with?('/') ? path[1..-1] : path
14
+ end
15
+ end
16
+
17
+ def call(env)
18
+ # Mark the request time
19
+ start = Time.now
20
+
21
+ # Process the request
22
+ status, headers, body = @app.call(env)
23
+
24
+ # Mark the response time
25
+ stop = Time.now
26
+
27
+ # Calculate total response time
28
+ response_time = (stop - start) * 1000
29
+
30
+ request = Rack::Request.new(env)
31
+ path = normalize_path(request.path_info)
32
+
33
+ # Emit stats to StatsD
34
+ ServiceTemplate::Stats.emitter.timing('response_time', response_time)
35
+ ServiceTemplate::Stats.emitter.timing("path.#{ServiceTemplate::Stats.path_to_key(request.request_method, path)}.response_time", response_time)
36
+
37
+ # Return the results
38
+ [status, headers, body]
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,15 @@
1
+ module ServiceTemplate
2
+ class Entity < Grape::Entity
3
+ def self.inherited(subclass)
4
+ ActiveSupport::Deprecation.warn 'Use of ServiceTemplate::Entity is discouraged, please transition your code to Roar representers - https://github.com/bellycard/service_template/blob/master/docs/grape_entity_to_roar.md', caller
5
+ end
6
+
7
+ format_with :to_s do |val|
8
+ val.to_s
9
+ end
10
+
11
+ def object_type
12
+ object.class.name.underscore
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,16 @@
1
+ # include this in your representer, and you will always return all defined keys (even if their value is nil)
2
+ module ServiceTemplate
3
+ module Representable
4
+ module IncludeNil
5
+ def self.included base
6
+ base.extend ClassMethods
7
+ end
8
+
9
+ module ClassMethods
10
+ def property(name, options={}, &block)
11
+ super(name, options.merge(render_nil: true), &block)
12
+ end
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,9 @@
1
+ require 'roar/decorator'
2
+ require 'roar/json/json_api'
3
+
4
+ module ServiceTemplate
5
+ class JsonApiRepresenter < Roar::Decorator
6
+ include Roar::JSON::JSONAPI
7
+
8
+ end
9
+ end
@@ -0,0 +1,30 @@
1
+ require 'action_dispatch/http/filter_parameters'
2
+
3
+ module ServiceTemplate
4
+ module ParamSanitizer
5
+ include ActionDispatch::Http::FilterParameters
6
+
7
+ mattr_accessor :filter_params
8
+
9
+ KV_REGEXP = '[^&;=]+'
10
+ PAIR_REGEXP = %r{(#{KV_REGEXP})=(#{KV_REGEXP})}
11
+
12
+ def filter_params
13
+ @@filter_params || []
14
+ end
15
+
16
+ def parameter_filter
17
+ parameter_filter_for(filter_params)
18
+ end
19
+
20
+ def filtered_parameters(params)
21
+ parameter_filter.filter(params)
22
+ end
23
+
24
+ def filtered_query_string(query_string)
25
+ query_string.gsub(PAIR_REGEXP) do |_|
26
+ parameter_filter.filter([[$1, $2]]).first.join("=")
27
+ end
28
+ end
29
+ end
30
+ end