service_template 0.5.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +21 -0
- data/.rubocop.yml +23 -0
- data/.travis.yml +13 -0
- data/CHANGELOG.md +64 -0
- data/Gemfile +4 -0
- data/LICENSE +24 -0
- data/README.md +217 -0
- data/Rakefile +9 -0
- data/bin/service_template +5 -0
- data/lib/service_template.rb +55 -0
- data/lib/service_template/active_record_extensions/notifications_subscriber.rb +17 -0
- data/lib/service_template/active_record_extensions/seeder.rb +14 -0
- data/lib/service_template/active_record_extensions/stats.rb +37 -0
- data/lib/service_template/authentication.rb +8 -0
- data/lib/service_template/cli.rb +111 -0
- data/lib/service_template/deploy.rb +98 -0
- data/lib/service_template/gem_dependency.rb +37 -0
- data/lib/service_template/generators.rb +3 -0
- data/lib/service_template/generators/api_generator.rb +30 -0
- data/lib/service_template/generators/readme_generator.rb +47 -0
- data/lib/service_template/generators/scaffold_generator.rb +29 -0
- data/lib/service_template/generators/templates/api/app/apis/%name_tableize%_api.rb.tt +40 -0
- data/lib/service_template/generators/templates/api/app/models/%name_underscore%.rb.tt +2 -0
- data/lib/service_template/generators/templates/api/app/representers/%name_underscore%_representer.rb.tt +4 -0
- data/lib/service_template/generators/templates/api/spec/apis/%name_tableize%_api_spec.rb.tt +16 -0
- data/lib/service_template/generators/templates/api/spec/models/%name_underscore%_spec.rb.tt +9 -0
- data/lib/service_template/generators/templates/readme/README.md.tt +55 -0
- data/lib/service_template/generators/templates/readme/spec/docs/readme_spec.rb +7 -0
- data/lib/service_template/generators/templates/scaffold/.env.development.tt +9 -0
- data/lib/service_template/generators/templates/scaffold/.env.test.tt +10 -0
- data/lib/service_template/generators/templates/scaffold/.gitignore.tt +13 -0
- data/lib/service_template/generators/templates/scaffold/.rubocop.yml +24 -0
- data/lib/service_template/generators/templates/scaffold/.ruby-version.tt +1 -0
- data/lib/service_template/generators/templates/scaffold/Gemfile.tt +29 -0
- data/lib/service_template/generators/templates/scaffold/README.md +3 -0
- data/lib/service_template/generators/templates/scaffold/Rakefile +21 -0
- data/lib/service_template/generators/templates/scaffold/app.rb +19 -0
- data/lib/service_template/generators/templates/scaffold/app/apis/application_api.rb +9 -0
- data/lib/service_template/generators/templates/scaffold/app/apis/hello_api.rb.tt +10 -0
- data/lib/service_template/generators/templates/scaffold/config.ru.tt +21 -0
- data/lib/service_template/generators/templates/scaffold/config/database.yml.tt +19 -0
- data/lib/service_template/generators/templates/scaffold/config/initializers/active_record.rb +5 -0
- data/lib/service_template/generators/templates/scaffold/db/schema.rb +11 -0
- data/lib/service_template/generators/templates/scaffold/lib/.keep +0 -0
- data/lib/service_template/generators/templates/scaffold/log/.keep +0 -0
- data/lib/service_template/generators/templates/scaffold/spec/apis/hello_api_spec.rb.tt +17 -0
- data/lib/service_template/generators/templates/scaffold/spec/factories/.gitkeep +0 -0
- data/lib/service_template/generators/templates/scaffold/spec/spec_helper.rb +47 -0
- data/lib/service_template/grape_extenders.rb +30 -0
- data/lib/service_template/grape_extensions/error_formatter.rb +18 -0
- data/lib/service_template/grape_extensions/grape_helpers.rb +27 -0
- data/lib/service_template/identity.rb +45 -0
- data/lib/service_template/json_error.rb +24 -0
- data/lib/service_template/logger/log_transaction.rb +17 -0
- data/lib/service_template/logger/logger.rb +42 -0
- data/lib/service_template/logger/parseable.rb +37 -0
- data/lib/service_template/middleware/app_monitor.rb +17 -0
- data/lib/service_template/middleware/authentication.rb +32 -0
- data/lib/service_template/middleware/database_stats.rb +15 -0
- data/lib/service_template/middleware/logger.rb +67 -0
- data/lib/service_template/middleware/request_stats.rb +42 -0
- data/lib/service_template/output_formatters/entity.rb +15 -0
- data/lib/service_template/output_formatters/include_nil.rb +16 -0
- data/lib/service_template/output_formatters/json_api_representer.rb +9 -0
- data/lib/service_template/param_sanitizer.rb +30 -0
- data/lib/service_template/rspec_extensions/response_helpers.rb +46 -0
- data/lib/service_template/setup.rb +36 -0
- data/lib/service_template/sortable_api.rb +17 -0
- data/lib/service_template/stats.rb +43 -0
- data/lib/service_template/stats_d_timer.rb +26 -0
- data/lib/service_template/version.rb +45 -0
- data/lib/tasks/deploy.rake +11 -0
- data/lib/tasks/routes.rake +11 -0
- data/service_template.gemspec +42 -0
- data/spec/active_record_extensions/filter_by_hash_spec.rb +23 -0
- data/spec/active_record_extensions/seeder_spec.rb +13 -0
- data/spec/authentication_spec.rb +17 -0
- data/spec/deprecations/application_api_spec.rb +19 -0
- data/spec/deprecations/entity_spec.rb +9 -0
- data/spec/deprecations/filter_by_hash_spec.rb +9 -0
- data/spec/deprecations/napa_setup_spec.rb +52 -0
- data/spec/generators/api_generator_spec.rb +63 -0
- data/spec/generators/migration_generator_spec.rb +105 -0
- data/spec/generators/readme_generator_spec.rb +35 -0
- data/spec/generators/scaffold_generator_spec.rb +90 -0
- data/spec/grape_extenders_spec.rb +50 -0
- data/spec/grape_extensions/error_formatter_spec.rb +29 -0
- data/spec/grape_extensions/include_nil_spec.rb +23 -0
- data/spec/identity_spec.rb +50 -0
- data/spec/json_error_spec.rb +33 -0
- data/spec/logger/log_transaction_spec.rb +34 -0
- data/spec/logger/logger_spec.rb +14 -0
- data/spec/logger/parseable_spec.rb +16 -0
- data/spec/middleware/authentication_spec.rb +54 -0
- data/spec/middleware/database_stats_spec.rb +64 -0
- data/spec/middleware/request_stats_spec.rb +21 -0
- data/spec/sortable_api_spec.rb +56 -0
- data/spec/spec_helper.rb +45 -0
- data/spec/stats_d_timer_spec.rb +23 -0
- data/spec/stats_spec.rb +66 -0
- data/spec/version_spec.rb +40 -0
- data/tasks/spec.rake +9 -0
- data/tasks/version.rake +51 -0
- 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,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
|