insights-api-common 3.0.0
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 +7 -0
- data/LICENSE.txt +202 -0
- data/README.md +102 -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/insights.rb +1 -0
- data/lib/insights/api/common.rb +12 -0
- data/lib/insights/api/common/application_controller_mixins/api_doc.rb +39 -0
- data/lib/insights/api/common/application_controller_mixins/common.rb +27 -0
- data/lib/insights/api/common/application_controller_mixins/exception_handling.rb +41 -0
- data/lib/insights/api/common/application_controller_mixins/openapi_enabled.rb +13 -0
- data/lib/insights/api/common/application_controller_mixins/parameters.rb +134 -0
- data/lib/insights/api/common/application_controller_mixins/request_body_validation.rb +48 -0
- data/lib/insights/api/common/application_controller_mixins/request_parameter_validation.rb +29 -0
- data/lib/insights/api/common/application_controller_mixins/request_path.rb +70 -0
- data/lib/insights/api/common/engine.rb +20 -0
- data/lib/insights/api/common/entitlement.rb +37 -0
- data/lib/insights/api/common/error_document.rb +29 -0
- data/lib/insights/api/common/filter.rb +175 -0
- data/lib/insights/api/common/graphql.rb +127 -0
- data/lib/insights/api/common/graphql/associated_records.rb +44 -0
- data/lib/insights/api/common/graphql/association_loader.rb +35 -0
- data/lib/insights/api/common/graphql/generator.rb +148 -0
- data/lib/insights/api/common/graphql/templates/model_type.erb +35 -0
- data/lib/insights/api/common/graphql/templates/query_type.erb +49 -0
- data/lib/insights/api/common/graphql/templates/schema.erb +6 -0
- data/lib/insights/api/common/graphql/types/big_int.rb +23 -0
- data/lib/insights/api/common/graphql/types/date_time.rb +16 -0
- data/lib/insights/api/common/graphql/types/query_filter.rb +16 -0
- data/lib/insights/api/common/graphql/types/query_sort_by.rb +16 -0
- data/lib/insights/api/common/inflections.rb +28 -0
- data/lib/insights/api/common/logging.rb +17 -0
- data/lib/insights/api/common/metrics.rb +39 -0
- data/lib/insights/api/common/middleware/web_server_metrics.rb +62 -0
- data/lib/insights/api/common/open_api.rb +2 -0
- data/lib/insights/api/common/open_api/docs.rb +54 -0
- data/lib/insights/api/common/open_api/docs/component_collection.rb +67 -0
- data/lib/insights/api/common/open_api/docs/doc_v3.rb +102 -0
- data/lib/insights/api/common/open_api/docs/object_definition.rb +39 -0
- data/lib/insights/api/common/open_api/generator.rb +520 -0
- data/lib/insights/api/common/open_api/serializer.rb +31 -0
- data/lib/insights/api/common/option_redirect_enhancements.rb +23 -0
- data/lib/insights/api/common/paginated_response.rb +108 -0
- data/lib/insights/api/common/rbac/access.rb +66 -0
- data/lib/insights/api/common/rbac/acl.rb +74 -0
- data/lib/insights/api/common/rbac/policies.rb +33 -0
- data/lib/insights/api/common/rbac/query_shared_resource.rb +45 -0
- data/lib/insights/api/common/rbac/roles.rb +77 -0
- data/lib/insights/api/common/rbac/seed.rb +140 -0
- data/lib/insights/api/common/rbac/service.rb +67 -0
- data/lib/insights/api/common/rbac/share_resource.rb +60 -0
- data/lib/insights/api/common/rbac/unshare_resource.rb +32 -0
- data/lib/insights/api/common/rbac/utilities.rb +30 -0
- data/lib/insights/api/common/request.rb +111 -0
- data/lib/insights/api/common/routing.rb +26 -0
- data/lib/insights/api/common/user.rb +48 -0
- data/lib/insights/api/common/version.rb +7 -0
- data/lib/tasks/insights/api/common_tasks.rake +4 -0
- data/spec/support/default_as_json.rb +17 -0
- data/spec/support/rbac_shared_contexts.rb +44 -0
- data/spec/support/requests_spec_helper.rb +7 -0
- data/spec/support/service_spec_helper.rb +26 -0
- data/spec/support/user_header_spec_helper.rb +68 -0
- metadata +403 -0
@@ -0,0 +1,148 @@
|
|
1
|
+
require "erb"
|
2
|
+
|
3
|
+
module Insights
|
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" ? "::Insights::API::Common::GraphQL::Types::DateTime" : "types.String"
|
33
|
+
when "number"
|
34
|
+
"types.Float"
|
35
|
+
when "boolean"
|
36
|
+
"types.Boolean"
|
37
|
+
when "integer"
|
38
|
+
"::Insights::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?("^/[^/]*/{[[a-z]*_]*id}/#{collection}$") &&
|
45
|
+
openapi_content.dig("paths", path, "get").present?
|
46
|
+
end
|
47
|
+
collection_associations = []
|
48
|
+
openapi_content["paths"].keys.each do |path|
|
49
|
+
subcollection_match = path.match("^/#{collection}/{[[a-z]*_]*id}/([^/]*)$")
|
50
|
+
next unless subcollection_match
|
51
|
+
|
52
|
+
subcollection = subcollection_match[1]
|
53
|
+
next unless openapi_content["paths"].keys.any? do |subcollection_path|
|
54
|
+
subcollection_path.match?("^/#{subcollection}/{[[a-z]*_]*id}$") &&
|
55
|
+
openapi_content.dig("paths", subcollection_path, "get").present?
|
56
|
+
end
|
57
|
+
|
58
|
+
collection_associations << subcollection
|
59
|
+
end
|
60
|
+
[collection_is_associated ? true : false, collection_associations.sort]
|
61
|
+
end
|
62
|
+
|
63
|
+
def self.collection_field_resolvers(schema_overlay, collection)
|
64
|
+
field_resolvers = {}
|
65
|
+
schema_overlay.keys.each do |collection_regex|
|
66
|
+
next unless collection.match(collection_regex)
|
67
|
+
|
68
|
+
field_resolvers.merge!(schema_overlay.fetch_path(collection_regex, "field_resolvers") || {})
|
69
|
+
end
|
70
|
+
field_resolvers
|
71
|
+
end
|
72
|
+
|
73
|
+
def self.collection_schema_overlay(schema_overlay, collection)
|
74
|
+
schema_overlay.keys.each_with_object({}) do |collection_regex, collection_schema_overlay|
|
75
|
+
next unless collection.match?(collection_regex)
|
76
|
+
|
77
|
+
collection_schema_overlay.merge!(schema_overlay[collection_regex] || {})
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
def self.init_schema(request, schema_overlay = {})
|
82
|
+
api_version = ::Insights::API::Common::GraphQL.version(request)
|
83
|
+
version_namespace = "V#{api_version.tr('.', 'x')}"
|
84
|
+
openapi_doc = ::Insights::API::Common::OpenApi::Docs.instance[api_version]
|
85
|
+
openapi_content = openapi_doc.content
|
86
|
+
|
87
|
+
graphql_namespace = if ::Insights::API::Common::GraphQL::Api.const_defined?(version_namespace, false)
|
88
|
+
::Insights::API::Common::GraphQL::Api.const_get(version_namespace)
|
89
|
+
else
|
90
|
+
::Insights::API::Common::GraphQL::Api.const_set(version_namespace, Module.new)
|
91
|
+
end
|
92
|
+
|
93
|
+
return graphql_namespace.const_get("Schema") if graphql_namespace.const_defined?("Schema", false)
|
94
|
+
|
95
|
+
resources = openapi_content["paths"].keys.sort
|
96
|
+
collections = []
|
97
|
+
resources.each do |resource|
|
98
|
+
next unless openapi_content.dig("paths", resource, "get") # we only care for queries
|
99
|
+
|
100
|
+
rmatch = resource.match("^/(.*/)?([^/]*)/{[[a-z]*_]*id}$")
|
101
|
+
next unless rmatch
|
102
|
+
|
103
|
+
collection = rmatch[2]
|
104
|
+
klass_name = collection.camelize.singularize
|
105
|
+
next if graphql_namespace.const_defined?("#{klass_name}Type", false)
|
106
|
+
|
107
|
+
_schema_name, this_schema = openapi_schema(openapi_doc, klass_name)
|
108
|
+
next if this_schema.nil? || this_schema["type"] != "object" || this_schema["properties"].nil?
|
109
|
+
|
110
|
+
collections << collection
|
111
|
+
|
112
|
+
model_class = klass_name.constantize
|
113
|
+
model_encrypted_columns_set = (model_class.try(:encrypted_columns) || []).to_set
|
114
|
+
|
115
|
+
model_properties = []
|
116
|
+
properties = this_schema["properties"]
|
117
|
+
properties.keys.sort.each do |property_name|
|
118
|
+
next if model_encrypted_columns_set.include?(property_name)
|
119
|
+
|
120
|
+
property_schema = properties[property_name]
|
121
|
+
property_schema = openapi_content.dig(*path_parts(property_schema["$ref"])) if property_schema["$ref"]
|
122
|
+
property_format = property_schema["format"] || ""
|
123
|
+
property_type = property_schema["type"]
|
124
|
+
description = property_schema["description"]
|
125
|
+
|
126
|
+
property_graphql_type = graphql_type(property_name, property_format, property_type)
|
127
|
+
model_properties << [property_name, property_graphql_type, description] if property_graphql_type
|
128
|
+
end
|
129
|
+
|
130
|
+
field_resolvers = collection_field_resolvers(schema_overlay, klass_name)
|
131
|
+
model_is_associated, model_associations = resource_associations(openapi_content, collection)
|
132
|
+
|
133
|
+
graphql_model_type_template = ERB.new(template("model_type"), nil, '<>').result(binding)
|
134
|
+
graphql_namespace.module_eval(graphql_model_type_template)
|
135
|
+
end
|
136
|
+
|
137
|
+
graphql_query_type_template = ERB.new(template("query_type"), nil, '<>').result(binding)
|
138
|
+
graphql_namespace.module_eval(graphql_query_type_template)
|
139
|
+
|
140
|
+
graphql_schema_template = ERB.new(template("schema"), nil, '<>').result(binding)
|
141
|
+
graphql_namespace.module_eval(graphql_schema_template)
|
142
|
+
graphql_namespace.const_get("Schema")
|
143
|
+
end
|
144
|
+
end
|
145
|
+
end
|
146
|
+
end
|
147
|
+
end
|
148
|
+
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
|
+
::Insights::API::Common::GraphQL::AssociationLoader.new(<%= klass_name.constantize %>, "<%= associations %>", args).load(obj)
|
31
|
+
}
|
32
|
+
end
|
33
|
+
<% end %>
|
34
|
+
<% end %>
|
35
|
+
end
|
@@ -0,0 +1,49 @@
|
|
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 = "::Insights::API::Common::GraphQL::Api::#{version_namespace}::#{klass_name}Type".constantize
|
13
|
+
|
14
|
+
collection_schema_overlay = ::Insights::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, ::Insights::API::Common::GraphQL::Types::QueryFilter, "The Query Filter for querying the #{collection}"
|
25
|
+
argument :sort_by, ::Insights::API::Common::GraphQL::Types::QuerySortBy, "The optional attributes to sort by. Provided as an array of attr[:asc] and attr:desc values"
|
26
|
+
|
27
|
+
resolve lambda { |_obj, args, ctx|
|
28
|
+
if base_query.present?
|
29
|
+
scope = base_query.call(model_class, ctx)
|
30
|
+
else
|
31
|
+
scope = model_class
|
32
|
+
end
|
33
|
+
|
34
|
+
if args[:filter]
|
35
|
+
openapi_doc = ::Insights::API::Common::OpenApi::Docs.instance["<%= api_version %>"]
|
36
|
+
openapi_schema_name, _schema = ::Insights::API::Common::GraphQL::Generator.openapi_schema(openapi_doc, klass_name)
|
37
|
+
scope = ::Insights::API::Common::Filter.new(
|
38
|
+
scope,
|
39
|
+
ActionController::Parameters.new(args[:filter]),
|
40
|
+
openapi_doc.definitions[openapi_schema_name]).apply
|
41
|
+
end
|
42
|
+
scope = ::Insights::API::Common::GraphQL.search_options(scope, args)
|
43
|
+
::Insights::API::Common::PaginatedResponse.new(
|
44
|
+
base_query: scope, request: nil, limit: args[:limit], offset: args[:offset], sort_by: args[:sort_by]
|
45
|
+
).records
|
46
|
+
}
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module Insights
|
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 Insights
|
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 Insights
|
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,16 @@
|
|
1
|
+
module Insights
|
2
|
+
module API
|
3
|
+
module Common
|
4
|
+
module GraphQL
|
5
|
+
module Types
|
6
|
+
QuerySortBy = ::GraphQL::ScalarType.define do
|
7
|
+
name "QuerySortBy"
|
8
|
+
description "The Query SortBy"
|
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 Insights
|
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 Insights
|
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 Insights
|
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 "insights/api/common/middleware/web_server_metrics"
|
34
|
+
Rails.application.middleware.unshift(Insights::API::Common::Middleware::WebServerMetrics, :metrics_prefix => prefix)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
module Insights
|
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
|