manageiq-api-common 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/LICENSE.txt +202 -0
- data/README.md +62 -0
- data/Rakefile +18 -0
- data/app/models/authentication.rb +19 -0
- data/app/models/concerns/encryption_concern.rb +52 -0
- data/app/models/encryption.rb +13 -0
- data/lib/generators/shared_utilities/migration_generator.rb +79 -0
- data/lib/generators/shared_utilities/orm_helper.rb +25 -0
- data/lib/generators/shared_utilities/templates/migration.rb +27 -0
- data/lib/generators/shared_utilities/templates/migration_existing.rb +28 -0
- data/lib/manageiq-api-common.rb +1 -0
- data/lib/manageiq/api/common.rb +13 -0
- data/lib/manageiq/api/common/api_error.rb +21 -0
- data/lib/manageiq/api/common/application_controller_mixins/api_doc.rb +39 -0
- data/lib/manageiq/api/common/application_controller_mixins/common.rb +27 -0
- data/lib/manageiq/api/common/application_controller_mixins/openapi_enabled.rb +13 -0
- data/lib/manageiq/api/common/application_controller_mixins/parameters.rb +113 -0
- data/lib/manageiq/api/common/application_controller_mixins/request_body_validation.rb +61 -0
- data/lib/manageiq/api/common/application_controller_mixins/request_path.rb +75 -0
- data/lib/manageiq/api/common/engine.rb +20 -0
- data/lib/manageiq/api/common/entitlement.rb +35 -0
- data/lib/manageiq/api/common/error_document.rb +29 -0
- data/lib/manageiq/api/common/filter.rb +160 -0
- data/lib/manageiq/api/common/graphql.rb +117 -0
- data/lib/manageiq/api/common/graphql/associated_records.rb +44 -0
- data/lib/manageiq/api/common/graphql/association_loader.rb +35 -0
- data/lib/manageiq/api/common/graphql/generator.rb +149 -0
- data/lib/manageiq/api/common/graphql/templates/model_type.erb +35 -0
- data/lib/manageiq/api/common/graphql/templates/query_type.erb +47 -0
- data/lib/manageiq/api/common/graphql/templates/schema.erb +6 -0
- data/lib/manageiq/api/common/graphql/types/big_int.rb +23 -0
- data/lib/manageiq/api/common/graphql/types/date_time.rb +16 -0
- data/lib/manageiq/api/common/graphql/types/query_filter.rb +16 -0
- data/lib/manageiq/api/common/inflections.rb +28 -0
- data/lib/manageiq/api/common/logging.rb +17 -0
- data/lib/manageiq/api/common/metrics.rb +39 -0
- data/lib/manageiq/api/common/middleware/web_server_metrics.rb +62 -0
- data/lib/manageiq/api/common/open_api.rb +2 -0
- data/lib/manageiq/api/common/open_api/docs.rb +54 -0
- data/lib/manageiq/api/common/open_api/docs/component_collection.rb +67 -0
- data/lib/manageiq/api/common/open_api/docs/doc_v3.rb +92 -0
- data/lib/manageiq/api/common/open_api/docs/object_definition.rb +27 -0
- data/lib/manageiq/api/common/open_api/generator.rb +441 -0
- data/lib/manageiq/api/common/open_api/serializer.rb +31 -0
- data/lib/manageiq/api/common/option_redirect_enhancements.rb +23 -0
- data/lib/manageiq/api/common/paginated_response.rb +92 -0
- data/lib/manageiq/api/common/request.rb +107 -0
- data/lib/manageiq/api/common/routing.rb +26 -0
- data/lib/manageiq/api/common/user.rb +48 -0
- data/lib/manageiq/api/common/version.rb +7 -0
- data/lib/tasks/manageiq/api/common_tasks.rake +4 -0
- data/spec/support/default_as_json.rb +17 -0
- data/spec/support/requests_spec_helper.rb +7 -0
- data/spec/support/user_header_spec_helper.rb +62 -0
- metadata +375 -0
@@ -0,0 +1,149 @@
|
|
1
|
+
require "erb"
|
2
|
+
|
3
|
+
module ManageIQ
|
4
|
+
module API
|
5
|
+
module Common
|
6
|
+
module GraphQL
|
7
|
+
module Generator
|
8
|
+
PARAMETERS_PATH = "/components/parameters".freeze
|
9
|
+
SCHEMAS_PATH = "/components/schemas".freeze
|
10
|
+
|
11
|
+
def self.openapi_schema(openapi_doc, klass_name)
|
12
|
+
schemas = openapi_doc.content.dig(*path_parts(SCHEMAS_PATH))
|
13
|
+
[klass_name, "#{klass_name}Out"].each do |name|
|
14
|
+
schema = schemas[name]
|
15
|
+
return [name, schema] if schema
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def self.path_parts(openapi_path)
|
20
|
+
openapi_path.split("/")[1..-1]
|
21
|
+
end
|
22
|
+
|
23
|
+
def self.template(type)
|
24
|
+
File.read(Pathname.new(__dir__).join(File.expand_path("templates", __dir__), "#{type}.erb").to_s)
|
25
|
+
end
|
26
|
+
|
27
|
+
def self.graphql_type(property_name, property_format, property_type)
|
28
|
+
return "!types.ID" if property_name == "id"
|
29
|
+
|
30
|
+
case property_type
|
31
|
+
when "string"
|
32
|
+
property_format == "date-time" ? "::ManageIQ::API::Common::GraphQL::Types::DateTime" : "types.String"
|
33
|
+
when "number"
|
34
|
+
"types.Float"
|
35
|
+
when "boolean"
|
36
|
+
"types.Boolean"
|
37
|
+
when "integer"
|
38
|
+
"::ManageIQ::API::Common::GraphQL::Types::BigInt"
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def self.resource_associations(openapi_content, collection)
|
43
|
+
collection_is_associated = openapi_content["paths"].keys.any? do |path|
|
44
|
+
path.match("^/[^/]*/{id}/#{collection}$") &&
|
45
|
+
openapi_content.dig("paths", path, "get")
|
46
|
+
end
|
47
|
+
collection_associations = []
|
48
|
+
openapi_content["paths"].keys.each do |path|
|
49
|
+
subcollection_match = path.match("^/#{collection}/{id}/([^/]*)$")
|
50
|
+
next unless subcollection_match
|
51
|
+
|
52
|
+
subcollection = subcollection_match[1]
|
53
|
+
next unless openapi_content.dig("paths", "/#{subcollection}/{id}", "get")
|
54
|
+
|
55
|
+
collection_associations << subcollection
|
56
|
+
end
|
57
|
+
[collection_is_associated ? true : false, collection_associations.sort]
|
58
|
+
end
|
59
|
+
|
60
|
+
def self.collection_field_resolvers(schema_overlay, collection)
|
61
|
+
field_resolvers = {}
|
62
|
+
schema_overlay.keys.each do |collection_regex|
|
63
|
+
next unless collection.match(collection_regex)
|
64
|
+
|
65
|
+
field_resolvers.merge!(schema_overlay.fetch_path(collection_regex, "field_resolvers") || {})
|
66
|
+
end
|
67
|
+
field_resolvers
|
68
|
+
end
|
69
|
+
|
70
|
+
def self.collection_schema_overlay(schema_overlay, collection)
|
71
|
+
schema_overlay.keys.each_with_object({}) do |collection_regex, collection_schema_overlay|
|
72
|
+
next unless collection.match?(collection_regex)
|
73
|
+
|
74
|
+
collection_schema_overlay.merge!(schema_overlay[collection_regex] || {})
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
def self.init_schema(request, schema_overlay = {})
|
79
|
+
api_version = ::ManageIQ::API::Common::GraphQL.version(request)
|
80
|
+
version_namespace = "V#{api_version.tr('.', 'x')}"
|
81
|
+
openapi_doc = ::ManageIQ::API::Common::OpenApi::Docs.instance[api_version]
|
82
|
+
openapi_content = openapi_doc.content
|
83
|
+
|
84
|
+
api_namespace = if ::Api.const_defined?(version_namespace, false)
|
85
|
+
::Api.const_get(version_namespace)
|
86
|
+
else
|
87
|
+
::Api.const_set(version_namespace, Module.new)
|
88
|
+
end
|
89
|
+
|
90
|
+
graphql_namespace = if api_namespace.const_defined?("GraphQL", false)
|
91
|
+
api_namespace.const_get("GraphQL")
|
92
|
+
else
|
93
|
+
api_namespace.const_set("GraphQL", Module.new)
|
94
|
+
end
|
95
|
+
|
96
|
+
return graphql_namespace.const_get("Schema") if graphql_namespace.const_defined?("Schema", false)
|
97
|
+
|
98
|
+
resources = openapi_content["paths"].keys.sort
|
99
|
+
collections = []
|
100
|
+
resources.each do |resource|
|
101
|
+
next unless openapi_content.dig("paths", resource, "get") # we only care for queries
|
102
|
+
|
103
|
+
rmatch = resource.match("^/(.*/)?([^/]*)/{id}$")
|
104
|
+
next unless rmatch
|
105
|
+
|
106
|
+
collection = rmatch[2]
|
107
|
+
klass_name = collection.camelize.singularize
|
108
|
+
_schema_name, this_schema = openapi_schema(openapi_doc, klass_name)
|
109
|
+
next if this_schema.nil? || this_schema["type"] != "object" || this_schema["properties"].nil?
|
110
|
+
|
111
|
+
collections << collection
|
112
|
+
|
113
|
+
model_class = klass_name.constantize
|
114
|
+
model_encrypted_columns_set = (model_class.try(:encrypted_columns) || []).to_set
|
115
|
+
|
116
|
+
model_properties = []
|
117
|
+
properties = this_schema["properties"]
|
118
|
+
properties.keys.sort.each do |property_name|
|
119
|
+
next if model_encrypted_columns_set.include?(property_name)
|
120
|
+
|
121
|
+
property_schema = properties[property_name]
|
122
|
+
property_schema = openapi_content.dig(*path_parts(property_schema["$ref"])) if property_schema["$ref"]
|
123
|
+
property_format = property_schema["format"] || ""
|
124
|
+
property_type = property_schema["type"]
|
125
|
+
description = property_schema["description"]
|
126
|
+
|
127
|
+
property_graphql_type = graphql_type(property_name, property_format, property_type)
|
128
|
+
model_properties << [property_name, property_graphql_type, description] if property_graphql_type
|
129
|
+
end
|
130
|
+
|
131
|
+
field_resolvers = collection_field_resolvers(schema_overlay, klass_name)
|
132
|
+
model_is_associated, model_associations = resource_associations(openapi_content, collection)
|
133
|
+
|
134
|
+
graphql_model_type_template = ERB.new(template("model_type"), nil, '<>').result(binding)
|
135
|
+
graphql_namespace.module_eval(graphql_model_type_template)
|
136
|
+
end
|
137
|
+
|
138
|
+
graphql_query_type_template = ERB.new(template("query_type"), nil, '<>').result(binding)
|
139
|
+
graphql_namespace.module_eval(graphql_query_type_template)
|
140
|
+
|
141
|
+
graphql_schema_template = ERB.new(template("schema"), nil, '<>').result(binding)
|
142
|
+
graphql_namespace.module_eval(graphql_schema_template)
|
143
|
+
graphql_namespace.const_get("Schema")
|
144
|
+
end
|
145
|
+
end
|
146
|
+
end
|
147
|
+
end
|
148
|
+
end
|
149
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
<%= klass_name %>Type = ::GraphQL::ObjectType.define do
|
2
|
+
name "<%= klass_name %>"
|
3
|
+
description "A <%= klass_name %>"
|
4
|
+
|
5
|
+
<% model_properties.each do |property| %>
|
6
|
+
<% property_name, property_type, property_description = property %>
|
7
|
+
<% field_description = property_description.nil? ? "" : ", \"#{property_description}\"" %>
|
8
|
+
<% field_resolver = "" %>
|
9
|
+
<% if field_resolvers[property_name].present? %>
|
10
|
+
<% field_resolver = "do\n resolve ->(obj, args, ctx) #{field_resolvers[property_name].strip}\n end"%>
|
11
|
+
<% end%>
|
12
|
+
field :<%= property_name %>, <%=property_type%> <%=field_description%> <%=field_resolver%>
|
13
|
+
<% end %>
|
14
|
+
<% if model_associations.present? %>
|
15
|
+
<% model_associations.each do |association| %>
|
16
|
+
<% associations = association.pluralize %>
|
17
|
+
<% association_class_name = association.camelize.singularize %>
|
18
|
+
|
19
|
+
field :<%= associations %> do
|
20
|
+
description "The <%= associations %> associated with this <%= klass_name %>"
|
21
|
+
type types[<%= "#{association_class_name}Type" %>]
|
22
|
+
|
23
|
+
argument :id, types.ID
|
24
|
+
argument :offset, types.Int, "The number of <%= associations %> to skip before starting to collect the result set"
|
25
|
+
argument :limit, types.Int, "The number of <%= associations %> to return"
|
26
|
+
|
27
|
+
preload :<%= associations %>
|
28
|
+
|
29
|
+
resolve lambda { |obj, args, _ctx|
|
30
|
+
::ManageIQ::API::Common::GraphQL::AssociationLoader.new(<%= klass_name.constantize %>, "<%= associations %>", args).load(obj)
|
31
|
+
}
|
32
|
+
end
|
33
|
+
<% end %>
|
34
|
+
<% end %>
|
35
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
QueryType = ::GraphQL::ObjectType.define do
|
2
|
+
name "Query"
|
3
|
+
description "The query root of this schema"
|
4
|
+
|
5
|
+
[
|
6
|
+
<%= collections.map { |c| ":#{c}" }.join(", ") %>
|
7
|
+
].each do |collection|
|
8
|
+
|
9
|
+
klass_names = collection.to_s.camelize
|
10
|
+
klass_name = klass_names.singularize
|
11
|
+
model_class = klass_name.constantize
|
12
|
+
resource_type = "Api::#{version_namespace}::GraphQL::#{klass_name}Type".constantize
|
13
|
+
|
14
|
+
collection_schema_overlay = ::ManageIQ::API::Common::GraphQL::Generator.collection_schema_overlay(schema_overlay, collection)
|
15
|
+
base_query = collection_schema_overlay["base_query"]
|
16
|
+
|
17
|
+
field collection do
|
18
|
+
description "List available #{collection}"
|
19
|
+
type types[resource_type]
|
20
|
+
|
21
|
+
argument :id, types.ID
|
22
|
+
argument :offset, types.Int, "The number of #{collection} to skip before starting to collect the result set"
|
23
|
+
argument :limit, types.Int, "The number of #{collection} to return"
|
24
|
+
argument :filter, ::ManageIQ::API::Common::GraphQL::Types::QueryFilter, "The Query Filter for querying the #{collection}"
|
25
|
+
resolve lambda { |_obj, args, ctx|
|
26
|
+
if base_query.present?
|
27
|
+
scope = base_query.call(model_class, ctx)
|
28
|
+
else
|
29
|
+
scope = model_class
|
30
|
+
end
|
31
|
+
|
32
|
+
if args[:filter]
|
33
|
+
openapi_doc = ::ManageIQ::API::Common::OpenApi::Docs.instance["<%= api_version %>"]
|
34
|
+
openapi_schema_name, _schema = ::ManageIQ::API::Common::GraphQL::Generator.openapi_schema(openapi_doc, klass_name)
|
35
|
+
scope = ::ManageIQ::API::Common::Filter.new(
|
36
|
+
scope,
|
37
|
+
ActionController::Parameters.new(args[:filter]),
|
38
|
+
openapi_doc.definitions[openapi_schema_name]).apply
|
39
|
+
end
|
40
|
+
scope = ::ManageIQ::API::Common::GraphQL.search_options(scope, args)
|
41
|
+
::ManageIQ::API::Common::PaginatedResponse.new(
|
42
|
+
base_query: scope, request: nil, limit: args[:limit], offset: args[:offset]
|
43
|
+
).records
|
44
|
+
}
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module ManageIQ
|
2
|
+
module API
|
3
|
+
module Common
|
4
|
+
module GraphQL
|
5
|
+
module Types
|
6
|
+
class BigInt < ::GraphQL::Schema::Scalar
|
7
|
+
description "Represents non-fractional signed whole numeric values. Since the value may exceed the size of a 32-bit integer, it's encoded as a string."
|
8
|
+
|
9
|
+
def self.coerce_input(value, _ctx)
|
10
|
+
Integer(value)
|
11
|
+
rescue ArgumentError
|
12
|
+
nil
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.coerce_result(value, _ctx)
|
16
|
+
value.to_i.to_s
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
module ManageIQ
|
2
|
+
module API
|
3
|
+
module Common
|
4
|
+
module GraphQL
|
5
|
+
module Types
|
6
|
+
DateTime = ::GraphQL::ScalarType.define do
|
7
|
+
name "DateTime"
|
8
|
+
description "An ISO-8601 encoded UTC date string."
|
9
|
+
|
10
|
+
coerce_input ->(value, _ctx) { Time.zone.parse(value) }
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
module ManageIQ
|
2
|
+
module API
|
3
|
+
module Common
|
4
|
+
module GraphQL
|
5
|
+
module Types
|
6
|
+
QueryFilter = ::GraphQL::ScalarType.define do
|
7
|
+
name "QueryFilter"
|
8
|
+
description "The Query Filter"
|
9
|
+
|
10
|
+
coerce_input ->(value, _ctx) { JSON.parse(value.to_json) }
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
module ManageIQ
|
2
|
+
module API
|
3
|
+
module Common
|
4
|
+
module Inflections
|
5
|
+
def self.load_inflections
|
6
|
+
@loaded ||= begin
|
7
|
+
load_common_inflections
|
8
|
+
true
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
def self.load_common_inflections
|
13
|
+
# Add new inflection rules using the following format
|
14
|
+
# (all these examples are active by default):
|
15
|
+
# ActiveSupport::Inflector.inflections do |inflect|
|
16
|
+
# inflect.plural /^(ox)$/i, '\1en'
|
17
|
+
# inflect.singular /^(ox)en/i, '\1'
|
18
|
+
# inflect.irregular 'person', 'people'
|
19
|
+
# inflect.uncountable %w( fish sheep )
|
20
|
+
# end
|
21
|
+
ActiveSupport::Inflector.inflections do |inflect|
|
22
|
+
inflect.acronym('ManageIQ')
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module ManageIQ
|
2
|
+
module API
|
3
|
+
module Common
|
4
|
+
class Logging
|
5
|
+
def self.activate(config)
|
6
|
+
require 'manageiq/loggers'
|
7
|
+
config.logger = if Rails.env.production?
|
8
|
+
config.colorize_logging = false
|
9
|
+
ManageIQ::Loggers::CloudWatch.new
|
10
|
+
else
|
11
|
+
ManageIQ::Loggers::Base.new(Rails.root.join("log", "#{Rails.env}.log"))
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
module ManageIQ
|
2
|
+
module API
|
3
|
+
module Common
|
4
|
+
class Metrics
|
5
|
+
def self.activate(config, prefix)
|
6
|
+
require 'prometheus_exporter'
|
7
|
+
require 'prometheus_exporter/client'
|
8
|
+
|
9
|
+
ensure_exporter_server
|
10
|
+
enable_in_process_metrics
|
11
|
+
enable_web_server_metrics(prefix)
|
12
|
+
end
|
13
|
+
|
14
|
+
private_class_method def self.ensure_exporter_server
|
15
|
+
require 'socket'
|
16
|
+
TCPSocket.open("localhost", 9394) {}
|
17
|
+
rescue Errno::ECONNREFUSED
|
18
|
+
require 'prometheus_exporter/server'
|
19
|
+
server = PrometheusExporter::Server::WebServer.new(port: 9394)
|
20
|
+
server.start
|
21
|
+
|
22
|
+
PrometheusExporter::Client.default = PrometheusExporter::LocalClient.new(collector: server.collector)
|
23
|
+
end
|
24
|
+
|
25
|
+
private_class_method def self.enable_in_process_metrics
|
26
|
+
require 'prometheus_exporter/instrumentation'
|
27
|
+
|
28
|
+
# this reports basic process metrics such as RSS and Ruby metrics
|
29
|
+
PrometheusExporter::Instrumentation::Process.start
|
30
|
+
end
|
31
|
+
|
32
|
+
private_class_method def self.enable_web_server_metrics(prefix)
|
33
|
+
require "manageiq/api/common/middleware/web_server_metrics"
|
34
|
+
Rails.application.middleware.unshift(ManageIQ::API::Common::Middleware::WebServerMetrics, :metrics_prefix => prefix)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
module ManageIQ
|
2
|
+
module API
|
3
|
+
module Common
|
4
|
+
module Middleware
|
5
|
+
class WebServerMetrics
|
6
|
+
def initialize(app, options = {})
|
7
|
+
@app = app
|
8
|
+
@metrics_prefix = options[:metrics_prefix] || "http_server"
|
9
|
+
|
10
|
+
require 'prometheus_exporter/client'
|
11
|
+
require 'prometheus_exporter/metric'
|
12
|
+
|
13
|
+
PrometheusExporter::Metric::Base.default_prefix = "#{@metrics_prefix}_"
|
14
|
+
|
15
|
+
@puma_busy_threads = PrometheusExporter::Client.default.register(:gauge, "puma_busy_threads", "The number of threads currently handling HTTP requests.")
|
16
|
+
@puma_max_threads = PrometheusExporter::Client.default.register(:gauge, "puma_max_threads", "The total number of threads able to handle HTTP requests.")
|
17
|
+
@request_counter = PrometheusExporter::Client.default.register(:counter, "requests_total", "The total number of HTTP requests handled by the Rack application.")
|
18
|
+
@request_histogram = PrometheusExporter::Client.default.register(:histogram, "request_duration_seconds", "The HTTP response duration of the Rack application.")
|
19
|
+
end
|
20
|
+
|
21
|
+
def call(env)
|
22
|
+
@puma_busy_threads.increment
|
23
|
+
|
24
|
+
result = nil
|
25
|
+
duration = Benchmark.realtime { result = @app.call(env) }
|
26
|
+
result
|
27
|
+
rescue => error
|
28
|
+
@error = error
|
29
|
+
raise
|
30
|
+
ensure
|
31
|
+
duration_labels = {
|
32
|
+
:method => env['REQUEST_METHOD'].downcase,
|
33
|
+
:path => strip_ids_from_path(env['PATH_INFO']),
|
34
|
+
}
|
35
|
+
|
36
|
+
counter_labels = duration_labels.merge(:code => result.first.to_s).tap do |labels|
|
37
|
+
labels[:exception] = @error.class.name if @error
|
38
|
+
end
|
39
|
+
|
40
|
+
@request_counter.increment(counter_labels)
|
41
|
+
@request_histogram.observe(duration, duration_labels)
|
42
|
+
|
43
|
+
@puma_max_threads.observe(puma_stats["max_threads"])
|
44
|
+
@puma_busy_threads.decrement
|
45
|
+
end
|
46
|
+
|
47
|
+
private
|
48
|
+
|
49
|
+
def strip_ids_from_path(path)
|
50
|
+
path.gsub(%r{/\d+(/|$)}, '/:id\\1')
|
51
|
+
end
|
52
|
+
|
53
|
+
def puma_stats
|
54
|
+
JSON.parse(Puma.stats)
|
55
|
+
rescue NoMethodError
|
56
|
+
{}
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|