hephaestus 0.0.1

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.
Files changed (91) hide show
  1. checksums.yaml +7 -0
  2. data/.ruby-version +1 -0
  3. data/CHANGELOG.md +13 -0
  4. data/LICENSE.txt +9 -0
  5. data/README.md +19 -0
  6. data/bin/hephaestus +55 -0
  7. data/lib/hephaestus/actions/strip_comments_action.rb +263 -0
  8. data/lib/hephaestus/actions.rb +116 -0
  9. data/lib/hephaestus/app_builder.rb +168 -0
  10. data/lib/hephaestus/exit_on_failure.rb +22 -0
  11. data/lib/hephaestus/generators/app_generator.rb +158 -0
  12. data/lib/hephaestus/generators/base.rb +65 -0
  13. data/lib/hephaestus/generators/config_generator.rb +102 -0
  14. data/lib/hephaestus/generators/core_generator.rb +50 -0
  15. data/lib/hephaestus/generators/deployment_generator.rb +18 -0
  16. data/lib/hephaestus/generators/lib_generator.rb +16 -0
  17. data/lib/hephaestus/generators/license_generator.rb +16 -0
  18. data/lib/hephaestus/generators/rubocop_generator.rb +18 -0
  19. data/lib/hephaestus/generators/sorbet_generator.rb +16 -0
  20. data/lib/hephaestus/version.rb +11 -0
  21. data/lib/hephaestus.rb +21 -0
  22. data/templates/Gemfile.erb +121 -0
  23. data/templates/Procfile.debug +2 -0
  24. data/templates/Procfile.dev +2 -0
  25. data/templates/README.md.erb +1 -0
  26. data/templates/app/controllers/application_controller.rb +107 -0
  27. data/templates/app/controllers/concerns/authable.rb +32 -0
  28. data/templates/app/controllers/root_controller.rb +11 -0
  29. data/templates/app/controllers/settings_controller.rb +7 -0
  30. data/templates/app/controllers/staff_controller.rb +15 -0
  31. data/templates/app/controllers/yetto_controller.rb +30 -0
  32. data/templates/app/jobs/application_job.rb +10 -0
  33. data/templates/app/jobs/update_yetto_job.rb +27 -0
  34. data/templates/app/lib/body_parameter/yetto_parameters.rb +8 -0
  35. data/templates/app/lib/body_parameter.rb +6 -0
  36. data/templates/app/lib/constants/app.rb +8 -0
  37. data/templates/app/lib/headers/yetto.rb +17 -0
  38. data/templates/app/lib/headers.rb +5 -0
  39. data/templates/app/lib/path_parameter/yetto_parameters.rb +25 -0
  40. data/templates/app/lib/path_parameter.rb +8 -0
  41. data/templates/app/lib/plug_app/http.rb +34 -0
  42. data/templates/app/lib/plug_app/middleware/malformed_request.rb +110 -0
  43. data/templates/app/lib/plug_app/middleware/not_found.rb +41 -0
  44. data/templates/app/lib/plug_app/middleware/openapi_validation.rb +54 -0
  45. data/templates/app/lib/plug_app/middleware/tracing_attributes.rb +42 -0
  46. data/templates/app/lib/query_parameter.rb +6 -0
  47. data/templates/app/serializers/error_serializer.rb +16 -0
  48. data/templates/app/services/yetto_service.rb +61 -0
  49. data/templates/app/views/settings/index.json.jbuilder +15 -0
  50. data/templates/config/initializers/cors.rb +18 -0
  51. data/templates/config/initializers/environment.rb +30 -0
  52. data/templates/config/initializers/filter_parameter_logging.rb +22 -0
  53. data/templates/config/initializers/inflections.rb +20 -0
  54. data/templates/config/initializers/lograge.rb +25 -0
  55. data/templates/config/initializers/open_telemetry.rb +27 -0
  56. data/templates/config/initializers/sidekiq.rb +11 -0
  57. data/templates/config/initializers/slack_webhook_logger.rb +17 -0
  58. data/templates/config/sidekiq.yml +18 -0
  59. data/templates/hephaestus_gitignore +296 -0
  60. data/templates/lib/plug_app/schemas/api/2023-03-06/components/parameters/headers/yetto.json +42 -0
  61. data/templates/lib/plug_app/schemas/api/2023-03-06/components/parameters/plugInstallation.json +12 -0
  62. data/templates/lib/plug_app/schemas/api/2023-03-06/components/schemas/plug.json +9 -0
  63. data/templates/lib/plug_app/schemas/api/2023-03-06/components/schemas/responses.json +64 -0
  64. data/templates/lib/plug_app/schemas/api/2023-03-06/components/schemas/yetto.json +1 -0
  65. data/templates/lib/plug_app/schemas/api/2023-03-06/openapi.json +27 -0
  66. data/templates/lib/plug_app/schemas/api/2023-03-06/paths/plug.json +91 -0
  67. data/templates/lib/plug_app/schemas/api/2023-03-06/paths/yetto/after_create_message.json +41 -0
  68. data/templates/lib/plug_app/schemas/api/2023-03-06/paths/yetto/after_create_plug_installation.json +41 -0
  69. data/templates/lib/tasks/test_tasks.rake +10 -0
  70. data/templates/script/ci +7 -0
  71. data/templates/script/hmac_text +22 -0
  72. data/templates/script/licenses +51 -0
  73. data/templates/script/ngrok +5 -0
  74. data/templates/script/security_checks/brakeman +5 -0
  75. data/templates/script/security_checks/bundle-audit +5 -0
  76. data/templates/script/server +5 -0
  77. data/templates/script/server-debug +5 -0
  78. data/templates/script/test +5 -0
  79. data/templates/script/typecheck +42 -0
  80. data/templates/sorbet/custom.rbi +14 -0
  81. data/templates/test/controllers/root_controller_test.rb +12 -0
  82. data/templates/test/controllers/settings_controller_test.rb +27 -0
  83. data/templates/test/controllers/yetto_controller_test.rb +130 -0
  84. data/templates/test/jobs/update_yetto_job_test.rb +41 -0
  85. data/templates/test/support/api.rb +74 -0
  86. data/templates/test/support/rails.rb +39 -0
  87. data/templates/test/support/webmocks/slack_webmock.rb +24 -0
  88. data/templates/test/support/webmocks/yetto.rb +94 -0
  89. data/templates/test/support/webmocks.rb +5 -0
  90. data/templates/test/test_helper.rb +24 -0
  91. metadata +209 -0
@@ -0,0 +1,107 @@
1
+ # typed: false
2
+ # frozen_string_literal: true
3
+
4
+ class ApplicationController < ActionController::Base
5
+ include ActionController::MimeResponds
6
+
7
+ rescue_from ActionController::UnknownFormat, with: :not_acceptable
8
+
9
+ before_action :set_request_span_context
10
+ def set_request_span_context
11
+ end
12
+
13
+ after_action :set_response_span_context
14
+ def set_response_span_context
15
+ OpenTelemetry::Trace.current_span.add_attributes({
16
+ OpenTelemetry::SemanticConventions::Trace::HTTP_RESPONSE_CONTENT_LENGTH => response.headers["content-length"] || 0,
17
+ })
18
+ end
19
+
20
+ def no_content
21
+ head(:no_content)
22
+ end
23
+
24
+ def okay
25
+ render(
26
+ json: {
27
+ message: "OK",
28
+ }.to_json,
29
+ status: :ok,
30
+ )
31
+ end
32
+
33
+ def created
34
+ render(
35
+ json: {
36
+ message: "Created",
37
+ }.to_json,
38
+ status: :created,
39
+ )
40
+ end
41
+
42
+ def bad_request
43
+ render(
44
+ json: {
45
+ errors: [
46
+ {
47
+ message: "Bad Request",
48
+ },
49
+ ],
50
+ }.to_json,
51
+ status: :bad_request,
52
+ )
53
+ end
54
+
55
+ def forbidden
56
+ render(
57
+ json: {
58
+ errors: [
59
+ {
60
+ message: "Forbidden",
61
+ },
62
+ ],
63
+ }.to_json,
64
+ status: :forbidden,
65
+ )
66
+ end
67
+
68
+ def not_acceptable(e)
69
+ render(
70
+ json: ::ErrorSerializer.format("Not Acceptable").to_json,
71
+ status: :not_acceptable,
72
+ )
73
+ end
74
+
75
+ def not_found
76
+ render(
77
+ json: ::ErrorSerializer.format("Not Found").to_json,
78
+ status: :not_found,
79
+ )
80
+ end
81
+
82
+ def service_unavailable(msg)
83
+ render(
84
+ json: {
85
+ errors: [
86
+ {
87
+ message: "Service Unavailable: #{msg}",
88
+ },
89
+ ],
90
+ }.to_json,
91
+ status: :service_unavailable,
92
+ )
93
+ end
94
+
95
+ def bad_gateway
96
+ render(
97
+ json: {
98
+ errors: [
99
+ {
100
+ message: "Bad Gateway",
101
+ },
102
+ ],
103
+ }.to_json,
104
+ status: :bad_gateway,
105
+ )
106
+ end
107
+ end
@@ -0,0 +1,32 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module Authable
5
+ extend T::Sig
6
+
7
+ include ActionDispatch::Http::Cache::Response
8
+
9
+ include ActionController::Helpers::ClassMethods
10
+ include ActionController::HttpAuthentication::Basic::ControllerMethods
11
+ include BodyParameter::YettoParameters
12
+
13
+ SHA256_DIGEST = OpenSSL::Digest.new("sha256")
14
+
15
+ sig { void }
16
+ def from_yetto?
17
+ return bad_request if request.headers.blank?
18
+
19
+ yetto_signature = request.headers.fetch(Headers::Yetto::HEADER_SIGNATURE, "")
20
+
21
+ return bad_request unless yetto_signature.start_with?("sha256=")
22
+
23
+ hmac_header = yetto_signature.split("sha256=").last
24
+ body = request.env["RAW_POST_DATA"]
25
+
26
+ calculated_hmac = OpenSSL::HMAC.hexdigest(SHA256_DIGEST, YETTO_PLUG_APP_TOKEN, body)
27
+
28
+ return true if ActiveSupport::SecurityUtils.secure_compare(calculated_hmac, hmac_header)
29
+
30
+ bad_request
31
+ end
32
+ end
@@ -0,0 +1,11 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ class RootController < ApplicationController
5
+ extend T::Sig
6
+
7
+ sig { void }
8
+ def index
9
+ okay
10
+ end
11
+ end
@@ -0,0 +1,7 @@
1
+ # typed: false
2
+ # frozen_string_literal: true
3
+
4
+ class SettingsController < ApplicationController
5
+ def index
6
+ end
7
+ end
@@ -0,0 +1,15 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ class StaffController < ApplicationController
5
+ extend T::Sig
6
+
7
+ class << self
8
+ extend T::Sig
9
+
10
+ sig { params(request: ActionDispatch::Request).returns(T::Boolean) }
11
+ def staff_request?(request)
12
+ false
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,30 @@
1
+ # typed: false
2
+ # frozen_string_literal: true
3
+
4
+ class YettoController < ApplicationController
5
+ include BodyParameter::YettoParameters
6
+ include PathParameter::YettoParameters
7
+ include Authable
8
+
9
+ include Headers::Yetto
10
+
11
+ before_action :from_yetto?
12
+
13
+ def event
14
+ case pparam_yetto_event
15
+ when Headers::Yetto::EVENT_AFTER_CREATE
16
+ case pparam_yetto_record_type
17
+ when Headers::Yetto::RECORD_TYPE_PLUG_INSTALLATION
18
+
19
+ no_content
20
+ when Headers::Yetto::RECORD_TYPE_MESSAGE
21
+
22
+ no_content
23
+ else
24
+ not_found
25
+ end
26
+ else
27
+ not_found
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,10 @@
1
+ # typed: false
2
+ # frozen_string_literal: true
3
+
4
+ class ApplicationJob < ActiveJob::Base
5
+ # Automatically retry jobs that encountered a deadlock
6
+ # retry_on ActiveRecord::Deadlocked
7
+
8
+ # Most jobs are safe to ignore if the underlying records are no longer available
9
+ # discard_on ActiveJob::DeserializationError
10
+ end
@@ -0,0 +1,27 @@
1
+ # typed: false
2
+ # frozen_string_literal: true
3
+
4
+ # Send updated data to Yetto to store in the database
5
+ # This can be used to update installation data or message data
6
+
7
+ class UpdateYettoJob < ApplicationJob
8
+ queue_as :default
9
+
10
+ def perform(params)
11
+ type = params.delete(:type)
12
+
13
+ organization_id = params.fetch(:organization, {}).fetch(:id, nil)
14
+ inbox_id = params.fetch(:inbox, {}).fetch(:id, nil)
15
+ plug_installation_id = params.fetch(:plug_installation, {}).fetch(:id, nil)
16
+
17
+ case type
18
+ when "installation"
19
+ YettoService.update_installation(organization_id, inbox_id, plug_installation_id, params)
20
+ when "switch"
21
+ plug_id = params.fetch(:plug, {}).fetch(:id, nil)
22
+ YettoService.create_switch(organization_id, inbox_id, plug_id, params)
23
+ when "message"
24
+ YettoService.create_message(organization_id, inbox_id, params)
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,8 @@
1
+ # typed: false
2
+ # frozen_string_literal: true
3
+
4
+ module BodyParameter
5
+ module YettoParameters
6
+ extend T::Sig
7
+ end
8
+ end
@@ -0,0 +1,6 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module BodyParameter
5
+ extend T::Sig
6
+ end
@@ -0,0 +1,8 @@
1
+ # typed: false
2
+ # frozen_string_literal: true
3
+
4
+ module Constants
5
+ module PlugApp
6
+ PLUG_APP_API_VERSION = "2023-03-06"
7
+ end
8
+ end
@@ -0,0 +1,17 @@
1
+ # typed: false
2
+ # frozen_string_literal: true
3
+
4
+ module Headers
5
+ module Yetto
6
+ YETTO_DELIVERY_ID = "HTTP_X_YETTO_DELIVERY_ID"
7
+
8
+ HEADER_EVENT = "HTTP_X_YETTO_EVENT"
9
+ EVENT_AFTER_CREATE = "after_create"
10
+
11
+ HEADER_RECORD_TYPE = "HTTP_X_YETTO_RECORD_TYPE"
12
+ RECORD_TYPE_PLUG_INSTALLATION = "plug_installation"
13
+ RECORD_TYPE_MESSAGE = "message"
14
+
15
+ HEADER_SIGNATURE = "HTTP_X_YETTO_SIGNATURE"
16
+ end
17
+ end
@@ -0,0 +1,5 @@
1
+ # typed: false
2
+ # frozen_string_literal: true
3
+
4
+ module Headers
5
+ end
@@ -0,0 +1,25 @@
1
+ # typed: false
2
+ # frozen_string_literal: true
3
+
4
+ module PathParameter
5
+ module YettoParameters
6
+ extend T::Sig
7
+
8
+ sig { returns(String) }
9
+ def pparam_yetto_event
10
+ yetto_path_params.fetch(:event, "")
11
+ end
12
+
13
+ sig { returns(String) }
14
+ def pparam_yetto_record_type
15
+ yetto_path_params.fetch(:record_type, "")
16
+ end
17
+
18
+ sig { returns(ActionController::Parameters) }
19
+ def yetto_path_params
20
+ return ActionController::Parameters.new if params.blank?
21
+
22
+ params.permit(:event, :record_type)
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,8 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module PathParameter
5
+ extend T::Sig
6
+
7
+ delegate :path_parameters, to: :request
8
+ end
@@ -0,0 +1,34 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module PlugApp
5
+ module HTTP
6
+ extend T::Sig
7
+
8
+ OK = "OK"
9
+ OK_I = 200
10
+
11
+ CREATED = "Created"
12
+ CREATED_I = 201
13
+ NO_CONTENT = "No Content"
14
+ NO_CONTENT_I = 204
15
+
16
+ NOT_FOUND = "Not Found"
17
+ NOT_FOUND_I = 404
18
+ BAD_REQUEST = "Bad Request"
19
+ BAD_REQUEST_I = 400
20
+ UNAUTHORIZED = "Unauthorized"
21
+ UNAUTHORIZED_I = 401
22
+ FORBIDDEN = "Forbidden"
23
+ FORBIDDEN_I = 403
24
+ NOT_ACCEPTABLE = "Not Acceptable"
25
+ NOT_ACCEPTABLE_I = 406
26
+ SERVICE_UNAVAILABLE = "Service Unavailable"
27
+ SERVICE_UNAVAILABLE_I = 503
28
+
29
+ sig { params(status: Integer).returns(T::Boolean) }
30
+ def status_ok?(status)
31
+ status == OK_I
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,110 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module PlugApp
5
+ module Middleware
6
+ # There is no valid reason for a request to contain a malformed string
7
+ # so just return HTTP 400 (Bad Request) if we receive one
8
+ class MalformedRequest
9
+ extend T::Sig
10
+
11
+ include ActionController::HttpAuthentication::Basic
12
+
13
+ NULL_BYTE_REGEX = T.let(Regexp.new(Regexp.escape("\u0000")).freeze, Regexp)
14
+
15
+ sig { returns(T.untyped) }
16
+ attr_reader :app
17
+
18
+ sig { params(app: T.untyped).void }
19
+ def initialize(app)
20
+ @app = T.let(app, T.untyped)
21
+ end
22
+
23
+ sig { params(env: T.untyped).returns(T.untyped) }
24
+ def call(env)
25
+ return [PlugApp::HTTP::BAD_REQUEST_I, { "Content-Type" => "text/plain" }, [PlugApp::HTTP::BAD_REQUEST]] if request_contains_malformed_string?(env)
26
+
27
+ app.call(env)
28
+ end
29
+
30
+ private
31
+
32
+ sig { params(env: T.untyped).returns(T::Boolean) }
33
+ def request_contains_malformed_string?(env)
34
+ # Duplicate the env, so it is not modified when accessing the parameters
35
+ # https://github.com/rails/rails/blob/34991a6ae2fc68347c01ea7382fa89004159e019/actionpack/lib/action_dispatch/http/parameters.rb#L59
36
+ request = ActionDispatch::Request.new(env.dup)
37
+
38
+ return true if malformed_path?(request.path)
39
+ return true if credentials_malformed?(request)
40
+
41
+ request.params.values.any? do |value|
42
+ param_has_null_byte?(value)
43
+ end
44
+ rescue ActionController::BadRequest
45
+ # If we can't build an ActionDispatch::Request something's wrong
46
+ # This would also happen if `#params` contains invalid UTF-8
47
+ # in this case we'll return a 400
48
+ true
49
+ end
50
+
51
+ sig { params(path: String).returns(T::Boolean) }
52
+ def malformed_path?(path)
53
+ string_malformed?(Rack::Utils.unescape(path))
54
+ rescue ArgumentError
55
+ # Rack::Utils.unescape raised this, path is malformed.
56
+ true
57
+ end
58
+
59
+ sig { params(request: T.untyped).returns(T::Boolean) }
60
+ def credentials_malformed?(request)
61
+ credentials = if has_basic_credentials?(request)
62
+ decode_credentials(request).presence
63
+ else
64
+ request.authorization.presence
65
+ end
66
+
67
+ return false unless credentials
68
+
69
+ string_malformed?(credentials)
70
+ end
71
+
72
+ sig { params(value: T.untyped, depth: Integer).returns(T::Boolean) }
73
+ def param_has_null_byte?(value, depth = 0)
74
+ # Guard against possible attack sending large amounts of nested params
75
+ # Should be safe as deeply nested params are highly uncommon.
76
+ return false if depth > 2
77
+
78
+ depth += 1
79
+
80
+ if value.respond_to?(:match)
81
+ string_malformed?(value)
82
+ elsif value.respond_to?(:values)
83
+ value.values.any? do |hash_value|
84
+ param_has_null_byte?(hash_value, depth)
85
+ end
86
+ elsif value.is_a?(Array)
87
+ value.any? do |array_value|
88
+ param_has_null_byte?(array_value, depth)
89
+ end
90
+ else
91
+ false
92
+ end
93
+ end
94
+
95
+ sig { params(string: String).returns(T::Boolean) }
96
+ def string_malformed?(string)
97
+ # We're using match instead of include because that raises an ArgumentError
98
+ # when the string contains invalid UTF-8
99
+ #
100
+ # We try to encode the string from ASCII-8BIT to UTF8. If we failed to do
101
+ # so for certain characters in the string, those chars are probably incomplete
102
+ # multibyte characters.
103
+ string.dup.force_encoding(Encoding::UTF_8).match?(NULL_BYTE_REGEX)
104
+ rescue ArgumentError, Encoding::UndefinedConversionError
105
+ # If we're here, we caught a malformed string. Return true
106
+ true
107
+ end
108
+ end
109
+ end
110
+ end
@@ -0,0 +1,41 @@
1
+ # typed: false
2
+ # frozen_string_literal: true
3
+
4
+ require "openapi_first"
5
+
6
+ module PlugApp
7
+ module Middleware
8
+ # frozen_string_literal: true
9
+
10
+ # Rack::NotFound is a default endpoint. Optionally initialize with the
11
+ # path to a custom 404 page, to override the standard response body.
12
+ #
13
+ # Examples:
14
+ #
15
+ # Serve default 404 response:
16
+ # run Rack::NotFound.new
17
+ #
18
+ # Serve a custom 404 page:
19
+ # run Rack::NotFound.new('path/to/your/404.html')
20
+
21
+ class NotFound
22
+ F = ::File
23
+
24
+ def initialize(path = nil, content_type = "text/html")
25
+ if path.nil?
26
+ @content = "Not found\n"
27
+ else
28
+ @content = F.read(path)
29
+ @content = F.read(path)
30
+ end
31
+ @length = @content.bytesize.to_s
32
+
33
+ @content_type = content_type
34
+ end
35
+
36
+ def call(env)
37
+ [404, { "Content-Type" => @content_type, "Content-Length" => @length }, [@content]]
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,54 @@
1
+ # typed: false
2
+ # frozen_string_literal: true
3
+
4
+ require "openapi_first"
5
+
6
+ module PlugApp
7
+ module Middleware
8
+ class OpenapiValidation
9
+ API_PATH_PREFIX = "/api/"
10
+ SPEC_PATH = Rails.root.join("lib/plug_app/schemas/api/2023-03-06/openapi.json")
11
+
12
+ def initialize(app)
13
+ @app = app
14
+ spec = OpenapiFirst.load(SPEC_PATH)
15
+ @response_validator = OpenapiFirst::ResponseValidator.new(spec)
16
+ @request_validator = OpenapiFirst::RequestValidation.new(->(_env) {}, spec: SPEC_PATH, raise_error: true)
17
+ end
18
+
19
+ def call(env)
20
+ request = Rack::Request.new(env)
21
+
22
+ if request.path.starts_with?(API_PATH_PREFIX)
23
+ # force content-type to JSON
24
+ if env["CONTENT_TYPE"] == "application/x-www-form-urlencoded"
25
+ env["CONTENT_TYPE"] = "application/json"
26
+ end
27
+
28
+ begin
29
+ @request_validator.call(env)
30
+ # response = @app.call(env)
31
+ # @response_validator.validate(request, response)
32
+ rescue OpenapiFirst::NotFoundError
33
+ PlugApp::Middleware::NotFound.new.call(env)
34
+ rescue OpenapiFirst::RequestInvalidError => e
35
+ error_hsh = ::ErrorSerializer.format(e.message)
36
+
37
+ return [PlugApp::HTTP::BAD_REQUEST_I, { "Content-Type" => "application/json" }, [error_hsh]]
38
+ rescue => e
39
+ raise e unless Rails.env.production?
40
+
41
+ logger.error(
42
+ "openapi.request_validation.error",
43
+ path: request.path,
44
+ method: request.env["REQUEST_METHOD"],
45
+ error_message: e.message,
46
+ )
47
+ end
48
+ end
49
+
50
+ @app.call(env)
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,42 @@
1
+ # typed: false
2
+ # frozen_string_literal: true
3
+
4
+ require "openapi_first"
5
+ require "active_support/parameter_filter"
6
+
7
+ module PlugApp
8
+ module Middleware
9
+ class TracingAttributes
10
+ extend T::Sig
11
+
12
+ sig { returns(T.untyped) }
13
+ attr_reader :app
14
+
15
+ HTTP_REQUEST_BODY = "http.request.body"
16
+
17
+ sig { params(app: T.untyped).void }
18
+ def initialize(app)
19
+ @app = T.let(app, T.untyped)
20
+ @filterer = ActiveSupport::ParameterFilter.new(Rails.application.config.filter_parameters)
21
+ end
22
+
23
+ sig { params(env: T.untyped).returns(T.untyped) }
24
+ def call(env)
25
+ request = ActionDispatch::Request.new(env.dup)
26
+
27
+ OpenTelemetry::Trace.current_span.add_attributes({
28
+ OpenTelemetry::VERSION => PlugApp::Application::GIT_SHA,
29
+ OpenTelemetry::SemanticConventions::Trace::HTTP_REQUEST_CONTENT_LENGTH => env["CONTENT_LENGTH"].to_i,
30
+ HTTP_REQUEST_BODY => filtered_params(request),
31
+ })
32
+
33
+ app.call(env)
34
+ end
35
+
36
+ def filtered_params(request)
37
+ params = request.params
38
+ @filterer.filter(params).to_json
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,6 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module QueryParameter
5
+ extend T::Sig
6
+ end
@@ -0,0 +1,16 @@
1
+ # typed: false
2
+ # frozen_string_literal: true
3
+
4
+ class ErrorSerializer
5
+ class << self
6
+ def format(message)
7
+ {
8
+ errors: [
9
+ {
10
+ message: message,
11
+ },
12
+ ],
13
+ }.to_json
14
+ end
15
+ end
16
+ end