hooks-ruby 0.0.2 → 0.0.4
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 +4 -4
- data/README.md +22 -6
- data/hooks.gemspec +0 -1
- data/lib/hooks/app/api.rb +59 -11
- data/lib/hooks/app/auth/auth.rb +15 -6
- data/lib/hooks/app/endpoints/catchall.rb +15 -3
- data/lib/hooks/app/endpoints/health.rb +2 -2
- data/lib/hooks/app/endpoints/version.rb +1 -1
- data/lib/hooks/app/helpers.rb +39 -9
- data/lib/hooks/core/builder.rb +6 -0
- data/lib/hooks/core/component_access.rb +69 -0
- data/lib/hooks/core/config_loader.rb +41 -9
- data/lib/hooks/core/config_validator.rb +3 -0
- data/lib/hooks/core/failbot.rb +50 -0
- data/lib/hooks/core/global_components.rb +51 -0
- data/lib/hooks/core/log.rb +15 -0
- data/lib/hooks/core/plugin_loader.rb +190 -4
- data/lib/hooks/core/stats.rb +54 -0
- data/lib/hooks/plugins/auth/base.rb +4 -12
- data/lib/hooks/plugins/auth/hmac.rb +20 -18
- data/lib/hooks/plugins/auth/timestamp_validator.rb +133 -0
- data/lib/hooks/plugins/handlers/base.rb +5 -12
- data/lib/hooks/plugins/handlers/default.rb +32 -3
- data/lib/hooks/plugins/instruments/failbot.rb +32 -0
- data/lib/hooks/plugins/instruments/failbot_base.rb +72 -0
- data/lib/hooks/plugins/instruments/stats.rb +32 -0
- data/lib/hooks/plugins/instruments/stats_base.rb +88 -0
- data/lib/hooks/plugins/lifecycle.rb +5 -0
- data/lib/hooks/utils/retry.rb +37 -0
- data/lib/hooks/version.rb +4 -1
- data/lib/hooks.rb +23 -10
- metadata +11 -21
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 2cab1bf1a1f61011d053be5beb9d48738bd439303b87743e41e10df0b4b9d65c
|
4
|
+
data.tar.gz: 9b9abc62fff8f0f2f77ad94949bc7e067fff5a2f072bbf73f8e6e55c03865714
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 40f769a697bd61c6c851eef217fadc66387c49d37c19639a283dd770ca8be8889a13eeba2785a41c53edcdef29327231527405a9de21b2b4620416196da792c2
|
7
|
+
data.tar.gz: 119a371d927f240aefe4e74c822b48318032250e7cbf060a9bd077b1375823137b419f7d1e2d27d775e6468d38843afb45ec9c8cd8a71a22d82226160c66043e
|
data/README.md
CHANGED
@@ -66,13 +66,13 @@ Here is a very high-level overview of how Hooks works:
|
|
66
66
|
status: "success",
|
67
67
|
handler: "MyCustomHandler",
|
68
68
|
payload_received: payload,
|
69
|
-
timestamp: Time.now.iso8601
|
69
|
+
timestamp: Time.now.utc.iso8601
|
70
70
|
}
|
71
71
|
end
|
72
72
|
end
|
73
73
|
```
|
74
74
|
|
75
|
-
That is pretty much it! Below you will find more detailed instructions on how to install and use Hooks, as well as how to create your own plugins. This high-level overview should give you a good idea of how Hooks works and how you can use it to handle webhooks in your applications. You may also be interested in using your own custom authentication plugins to secure your webhook endpoints, which is covered in the [Authentication](#authentication) section below.
|
75
|
+
That is pretty much it! Below you will find more detailed instructions on how to install and use Hooks, as well as how to create your own plugins. This high-level overview should give you a good idea of how Hooks works and how you can use it to handle webhooks in your applications. You may also be interested in using your own custom authentication plugins to secure your webhook endpoints, which is covered in the [Authentication](#authentication-plugins) section below.
|
76
76
|
|
77
77
|
## Installation 💎
|
78
78
|
|
@@ -102,7 +102,7 @@ First, create a `config.ru` file:
|
|
102
102
|
|
103
103
|
```ruby
|
104
104
|
# file: config.ru
|
105
|
-
require "hooks
|
105
|
+
require "hooks"
|
106
106
|
|
107
107
|
# See the config documentation below for the full list of available options
|
108
108
|
# For this example, we will just set use_catchall_route to true
|
@@ -229,7 +229,7 @@ class HelloHandler < Hooks::Plugins::Handlers::Base
|
|
229
229
|
{
|
230
230
|
message: "webhook processed successfully",
|
231
231
|
handler: "HelloHandler",
|
232
|
-
timestamp: Time.now.iso8601
|
232
|
+
timestamp: Time.now.utc.iso8601
|
233
233
|
}
|
234
234
|
end
|
235
235
|
end
|
@@ -245,7 +245,7 @@ class GoodbyeHandler < Hooks::Plugins::Handlers::Base
|
|
245
245
|
{
|
246
246
|
message: "goodbye webhook processed successfully",
|
247
247
|
handler: "GoodbyeHandler",
|
248
|
-
timestamp: Time.now.iso8601
|
248
|
+
timestamp: Time.now.utc.iso8601
|
249
249
|
}
|
250
250
|
end
|
251
251
|
end
|
@@ -286,10 +286,26 @@ What these steps have done is set up a Hooks server that listens for incoming we
|
|
286
286
|
|
287
287
|
To see a live working version of this example, check out the [`spec/acceptance/`](spec/acceptance/) directory in this repository, which is used for acceptance testing the Hooks framework. It contains a complete example of how to set up a Hooks server with custom plugins, authentication, and more.
|
288
288
|
|
289
|
-
### Authentication
|
289
|
+
### Authentication Plugins
|
290
290
|
|
291
291
|
See the [Auth Plugins](docs/auth_plugins.md) documentation for even more information on how to create your own custom authentication plugins.
|
292
292
|
|
293
|
+
### Handler Plugins
|
294
|
+
|
295
|
+
See the [Handler Plugins](docs/handler_plugins.md) documentation for in-depth information about handler plugins and how you can create your own to extend the functionality of the Hooks framework for your own deployment.
|
296
|
+
|
297
|
+
### Lifecycle Plugins
|
298
|
+
|
299
|
+
See the [Lifecycle Plugins](docs/lifecycle_plugins.md) documentation for information on how to create lifecycle plugins that can hook into the request/response/error lifecycle of the Hooks framework, allowing you to add custom behavior at various stages of processing webhook requests.
|
300
|
+
|
301
|
+
### Instrument Plugins
|
302
|
+
|
303
|
+
See the [Instrument Plugins](docs/instrument_plugins.md) documentation for information on how to create instrument plugins that can be used to collect metrics or report exceptions during webhook processing. These plugins can be used to integrate with monitoring and alerting systems.
|
304
|
+
|
305
|
+
### Configuration
|
306
|
+
|
307
|
+
See the [Configuration](docs/configuration.md) documentation for detailed information on how to configure your Hooks server, including global options, endpoint options, and more.
|
308
|
+
|
293
309
|
## Contributing 🤝
|
294
310
|
|
295
311
|
See the [Contributing](CONTRIBUTING.md) document for information on how to contribute to the Hooks project, including setting up your development environment, running tests, and releasing new versions.
|
data/hooks.gemspec
CHANGED
@@ -22,7 +22,6 @@ Gem::Specification.new do |spec|
|
|
22
22
|
spec.add_dependency "retryable", "~> 3.0", ">= 3.0.5"
|
23
23
|
spec.add_dependency "dry-schema", "~> 1.14", ">= 1.14.1"
|
24
24
|
spec.add_dependency "grape", "~> 2.3"
|
25
|
-
spec.add_dependency "grape-swagger", "~> 2.1", ">= 2.1.2"
|
26
25
|
spec.add_dependency "puma", "~> 6.6"
|
27
26
|
|
28
27
|
spec.required_ruby_version = Gem::Requirement.new(">= 3.2.2")
|
data/lib/hooks/app/api.rb
CHANGED
@@ -9,6 +9,7 @@ require_relative "../plugins/handlers/base"
|
|
9
9
|
require_relative "../plugins/handlers/default"
|
10
10
|
require_relative "../core/logger_factory"
|
11
11
|
require_relative "../core/log"
|
12
|
+
require_relative "../core/plugin_loader"
|
12
13
|
|
13
14
|
# Import all core endpoint classes dynamically
|
14
15
|
Dir[File.join(__dir__, "endpoints/**/*.rb")].sort.each { |file| require file }
|
@@ -21,14 +22,13 @@ module Hooks
|
|
21
22
|
include Hooks::App::Auth
|
22
23
|
|
23
24
|
class << self
|
24
|
-
attr_reader :
|
25
|
+
attr_reader :server_start_time
|
25
26
|
end
|
26
27
|
|
27
28
|
# Create a new configured API class
|
28
29
|
def self.create(config:, endpoints:, log:)
|
29
|
-
|
30
|
-
|
31
|
-
Hooks::Log.instance = log
|
30
|
+
# :nocov:
|
31
|
+
@server_start_time = Time.now
|
32
32
|
|
33
33
|
api_class = Class.new(Grape::API) do
|
34
34
|
content_type :json, "application/json"
|
@@ -48,23 +48,57 @@ module Hooks
|
|
48
48
|
endpoints.each do |endpoint_config|
|
49
49
|
full_path = "#{config[:root_path]}#{endpoint_config[:path]}"
|
50
50
|
handler_class_name = endpoint_config[:handler]
|
51
|
+
http_method = (endpoint_config[:method] || "post").downcase.to_sym
|
51
52
|
|
52
|
-
|
53
|
+
send(http_method, full_path) do
|
53
54
|
request_id = uuid
|
55
|
+
start_time = Time.now
|
56
|
+
|
54
57
|
request_context = {
|
55
58
|
request_id:,
|
56
59
|
path: full_path,
|
57
60
|
handler: handler_class_name
|
58
61
|
}
|
59
62
|
|
63
|
+
# everything wrapped in the log context has access to the request context and includes it in log messages
|
64
|
+
# ex: Hooks::Log.info("message") will include request_id, path, handler, etc
|
60
65
|
Core::LogContext.with(request_context) do
|
61
66
|
begin
|
67
|
+
# Build Rack environment for lifecycle hooks
|
68
|
+
rack_env = {
|
69
|
+
"REQUEST_METHOD" => request.request_method,
|
70
|
+
"PATH_INFO" => request.path_info,
|
71
|
+
"QUERY_STRING" => request.query_string,
|
72
|
+
"HTTP_VERSION" => request.env["HTTP_VERSION"],
|
73
|
+
"REQUEST_URI" => request.url,
|
74
|
+
"SERVER_NAME" => request.env["SERVER_NAME"],
|
75
|
+
"SERVER_PORT" => request.env["SERVER_PORT"],
|
76
|
+
"CONTENT_TYPE" => request.content_type,
|
77
|
+
"CONTENT_LENGTH" => request.content_length,
|
78
|
+
"REMOTE_ADDR" => request.env["REMOTE_ADDR"],
|
79
|
+
"hooks.request_id" => request_id,
|
80
|
+
"hooks.handler" => handler_class_name,
|
81
|
+
"hooks.endpoint_config" => endpoint_config,
|
82
|
+
"hooks.start_time" => start_time.iso8601,
|
83
|
+
"hooks.full_path" => full_path
|
84
|
+
}
|
85
|
+
|
86
|
+
# Add HTTP headers to environment
|
87
|
+
headers.each do |key, value|
|
88
|
+
env_key = "HTTP_#{key.upcase.tr('-', '_')}"
|
89
|
+
rack_env[env_key] = value
|
90
|
+
end
|
91
|
+
|
92
|
+
# Call lifecycle hooks: on_request
|
93
|
+
Core::PluginLoader.lifecycle_plugins.each do |plugin|
|
94
|
+
plugin.on_request(rack_env)
|
95
|
+
end
|
96
|
+
|
62
97
|
enforce_request_limits(config)
|
63
98
|
request.body.rewind
|
64
99
|
raw_body = request.body.read
|
65
100
|
|
66
101
|
if endpoint_config[:auth]
|
67
|
-
log.info "validating request (id: #{request_id}, handler: #{handler_class_name})"
|
68
102
|
validate_auth!(raw_body, headers, endpoint_config, config)
|
69
103
|
end
|
70
104
|
|
@@ -78,16 +112,29 @@ module Hooks
|
|
78
112
|
config: endpoint_config
|
79
113
|
)
|
80
114
|
|
81
|
-
|
115
|
+
# Call lifecycle hooks: on_response
|
116
|
+
Core::PluginLoader.lifecycle_plugins.each do |plugin|
|
117
|
+
plugin.on_response(rack_env, response)
|
118
|
+
end
|
119
|
+
|
120
|
+
log.info("successfully processed webhook event with handler: #{handler_class_name}")
|
121
|
+
log.debug("processing duration: #{Time.now - start_time}s")
|
82
122
|
status 200
|
83
123
|
content_type "application/json"
|
84
|
-
|
85
|
-
rescue => e
|
86
|
-
|
124
|
+
response.to_json
|
125
|
+
rescue StandardError => e
|
126
|
+
# Call lifecycle hooks: on_error
|
127
|
+
if defined?(rack_env)
|
128
|
+
Core::PluginLoader.lifecycle_plugins.each do |plugin|
|
129
|
+
plugin.on_error(e, rack_env)
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
log.error("an error occuring during the processing of a webhook event - #{e.message}")
|
87
134
|
error_response = {
|
88
135
|
error: e.message,
|
89
136
|
code: determine_error_code(e),
|
90
|
-
request_id:
|
137
|
+
request_id:
|
91
138
|
}
|
92
139
|
error_response[:backtrace] = e.backtrace unless config[:production]
|
93
140
|
status error_response[:code]
|
@@ -106,6 +153,7 @@ module Hooks
|
|
106
153
|
end
|
107
154
|
|
108
155
|
api_class
|
156
|
+
# :nocov:
|
109
157
|
end
|
110
158
|
end
|
111
159
|
end
|
data/lib/hooks/app/auth/auth.rb
CHANGED
@@ -22,7 +22,7 @@ module Hooks
|
|
22
22
|
def validate_auth!(payload, headers, endpoint_config, global_config = {})
|
23
23
|
auth_config = endpoint_config[:auth]
|
24
24
|
|
25
|
-
#
|
25
|
+
# Ensure auth type is present and valid
|
26
26
|
auth_type = auth_config&.dig(:type)
|
27
27
|
unless auth_type&.is_a?(String) && !auth_type.strip.empty?
|
28
28
|
error!("authentication configuration missing or invalid", 500)
|
@@ -32,17 +32,26 @@ module Hooks
|
|
32
32
|
begin
|
33
33
|
auth_class = Core::PluginLoader.get_auth_plugin(auth_type)
|
34
34
|
rescue => e
|
35
|
+
log.error("failed to load auth plugin '#{auth_type}': #{e.message}")
|
35
36
|
error!("unsupported auth type '#{auth_type}'", 400)
|
36
37
|
end
|
37
38
|
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
config: endpoint_config
|
42
|
-
)
|
39
|
+
log.debug("validating auth for request with auth_class: #{auth_class.name}")
|
40
|
+
unless auth_class.valid?(payload:, headers:, config: endpoint_config)
|
41
|
+
log.warn("authentication failed for request with auth_class: #{auth_class.name}")
|
43
42
|
error!("authentication failed", 401)
|
44
43
|
end
|
45
44
|
end
|
45
|
+
|
46
|
+
private
|
47
|
+
|
48
|
+
# Short logger accessor for auth module
|
49
|
+
# @return [Hooks::Log] Logger instance
|
50
|
+
#
|
51
|
+
# Provides access to the application logger for authentication operations.
|
52
|
+
def log
|
53
|
+
Hooks::Log.instance
|
54
|
+
end
|
46
55
|
end
|
47
56
|
end
|
48
57
|
end
|
@@ -1,5 +1,13 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
# !!! IMPORTANT !!!
|
4
|
+
# This file handles the catchall endpoint for the Hooks application.
|
5
|
+
# You should not be using catchall endpoints in production.
|
6
|
+
# This is mainly for development, testing, and demo purposes.
|
7
|
+
# The logging is limited, lifecycle hooks are not called,
|
8
|
+
# and it does not support plugins or instruments.
|
9
|
+
# Use with caution!
|
10
|
+
|
3
11
|
require "grape"
|
4
12
|
require_relative "../../plugins/handlers/default"
|
5
13
|
require_relative "../helpers"
|
@@ -10,10 +18,13 @@ module Hooks
|
|
10
18
|
include Hooks::App::Helpers
|
11
19
|
|
12
20
|
def self.mount_path(config)
|
21
|
+
# :nocov:
|
13
22
|
"#{config[:root_path]}/*path"
|
23
|
+
# :nocov:
|
14
24
|
end
|
15
25
|
|
16
26
|
def self.route_block(captured_config, captured_logger)
|
27
|
+
# :nocov:
|
17
28
|
proc do
|
18
29
|
request_id = uuid
|
19
30
|
|
@@ -23,7 +34,7 @@ module Hooks
|
|
23
34
|
|
24
35
|
# Set request context for logging
|
25
36
|
request_context = {
|
26
|
-
request_id
|
37
|
+
request_id:,
|
27
38
|
path: "/#{params[:path]}",
|
28
39
|
handler: "DefaultHandler"
|
29
40
|
}
|
@@ -45,8 +56,8 @@ module Hooks
|
|
45
56
|
|
46
57
|
# Call handler
|
47
58
|
response = handler.call(
|
48
|
-
payload
|
49
|
-
headers
|
59
|
+
payload:,
|
60
|
+
headers:,
|
50
61
|
config: {}
|
51
62
|
)
|
52
63
|
|
@@ -78,6 +89,7 @@ module Hooks
|
|
78
89
|
end
|
79
90
|
end
|
80
91
|
end
|
92
|
+
# :nocov:
|
81
93
|
end
|
82
94
|
end
|
83
95
|
end
|
@@ -10,9 +10,9 @@ module Hooks
|
|
10
10
|
content_type "application/json"
|
11
11
|
{
|
12
12
|
status: "healthy",
|
13
|
-
timestamp: Time.now.iso8601,
|
13
|
+
timestamp: Time.now.utc.iso8601,
|
14
14
|
version: Hooks::VERSION,
|
15
|
-
uptime_seconds: (Time.now - Hooks::App::API.
|
15
|
+
uptime_seconds: (Time.now - Hooks::App::API.server_start_time).to_i
|
16
16
|
}.to_json
|
17
17
|
end
|
18
18
|
end
|
data/lib/hooks/app/helpers.rb
CHANGED
@@ -21,13 +21,15 @@ module Hooks
|
|
21
21
|
# @return [void]
|
22
22
|
# @note Timeout enforcement should be handled at the server level (e.g., Puma)
|
23
23
|
def enforce_request_limits(config)
|
24
|
-
#
|
25
|
-
content_length =
|
26
|
-
headers["content-length"] || headers["HTTP_CONTENT_LENGTH"] ||
|
27
|
-
env["CONTENT_LENGTH"] || env["HTTP_CONTENT_LENGTH"]
|
24
|
+
# Optimized content length check - check most common sources first
|
25
|
+
content_length = request.content_length if respond_to?(:request) && request.respond_to?(:content_length)
|
28
26
|
|
29
|
-
|
30
|
-
|
27
|
+
content_length ||= headers["Content-Length"] ||
|
28
|
+
headers["CONTENT_LENGTH"] ||
|
29
|
+
headers["content-length"] ||
|
30
|
+
headers["HTTP_CONTENT_LENGTH"] ||
|
31
|
+
env["CONTENT_LENGTH"] ||
|
32
|
+
env["HTTP_CONTENT_LENGTH"]
|
31
33
|
|
32
34
|
content_length = content_length&.to_i
|
33
35
|
|
@@ -45,16 +47,21 @@ module Hooks
|
|
45
47
|
# @param symbolize [Boolean] Whether to symbolize keys in parsed JSON (default: true)
|
46
48
|
# @return [Hash, String] Parsed JSON as Hash (optionally symbolized), or raw body if not JSON
|
47
49
|
def parse_payload(raw_body, headers, symbolize: true)
|
50
|
+
# Optimized content type check - check most common header first
|
48
51
|
content_type = headers["Content-Type"] || headers["CONTENT_TYPE"] || headers["content-type"] || headers["HTTP_CONTENT_TYPE"]
|
49
52
|
|
50
53
|
# Try to parse as JSON if content type suggests it or if it looks like JSON
|
51
54
|
if content_type&.include?("application/json") || (raw_body.strip.start_with?("{", "[") rescue false)
|
52
55
|
begin
|
53
|
-
|
56
|
+
# Security: Limit JSON parsing depth and complexity to prevent JSON bombs
|
57
|
+
parsed_payload = safe_json_parse(raw_body)
|
54
58
|
parsed_payload = parsed_payload.transform_keys(&:to_sym) if symbolize && parsed_payload.is_a?(Hash)
|
55
59
|
return parsed_payload
|
56
|
-
rescue JSON::ParserError
|
57
|
-
# If JSON parsing fails, return raw body
|
60
|
+
rescue JSON::ParserError, ArgumentError => e
|
61
|
+
# If JSON parsing fails or security limits exceeded, return raw body
|
62
|
+
if e.message.include?("nesting") || e.message.include?("depth")
|
63
|
+
log.warn("JSON parsing limit exceeded: #{e.message}")
|
64
|
+
end
|
58
65
|
end
|
59
66
|
end
|
60
67
|
|
@@ -79,6 +86,29 @@ module Hooks
|
|
79
86
|
|
80
87
|
private
|
81
88
|
|
89
|
+
# Safely parse JSON
|
90
|
+
#
|
91
|
+
# @param json_string [String] The JSON string to parse
|
92
|
+
# @return [Hash, Array] Parsed JSON object
|
93
|
+
# @raise [JSON::ParserError] If JSON is invalid
|
94
|
+
# @raise [ArgumentError] If security limits are exceeded
|
95
|
+
def safe_json_parse(json_string)
|
96
|
+
# Security limits for JSON parsing
|
97
|
+
max_nesting = ENV.fetch("JSON_MAX_NESTING", "20").to_i
|
98
|
+
|
99
|
+
# Additional size check before parsing
|
100
|
+
if json_string.length > ENV.fetch("JSON_MAX_SIZE", "10485760").to_i # 10MB default
|
101
|
+
raise ArgumentError, "JSON payload too large for parsing"
|
102
|
+
end
|
103
|
+
|
104
|
+
JSON.parse(json_string, {
|
105
|
+
max_nesting: max_nesting,
|
106
|
+
create_additions: false, # Security: Disable object creation from JSON
|
107
|
+
object_class: Hash, # Use plain Hash instead of custom classes
|
108
|
+
array_class: Array # Use plain Array instead of custom classes
|
109
|
+
})
|
110
|
+
end
|
111
|
+
|
82
112
|
# Determine HTTP error code from exception
|
83
113
|
#
|
84
114
|
# @param exception [Exception] The exception to map to an HTTP status code
|
data/lib/hooks/core/builder.rb
CHANGED
@@ -5,6 +5,7 @@ require_relative "config_validator"
|
|
5
5
|
require_relative "logger_factory"
|
6
6
|
require_relative "plugin_loader"
|
7
7
|
require_relative "../app/api"
|
8
|
+
require_relative "../utils/retry"
|
8
9
|
|
9
10
|
module Hooks
|
10
11
|
module Core
|
@@ -34,6 +35,11 @@ module Hooks
|
|
34
35
|
)
|
35
36
|
end
|
36
37
|
|
38
|
+
Hooks::Log.instance = @log
|
39
|
+
|
40
|
+
# Hydrate our Retryable instance
|
41
|
+
Retry.setup!(log: @log)
|
42
|
+
|
37
43
|
# Load all plugins at boot time
|
38
44
|
load_plugins(config)
|
39
45
|
|
@@ -0,0 +1,69 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Hooks
|
4
|
+
module Core
|
5
|
+
# Shared module providing access to global components (logger, stats, failbot)
|
6
|
+
#
|
7
|
+
# This module provides a consistent interface for accessing global components
|
8
|
+
# across all plugin types, eliminating code duplication and ensuring consistent
|
9
|
+
# behavior throughout the application.
|
10
|
+
#
|
11
|
+
# @example Usage in a class that needs instance methods
|
12
|
+
# class MyHandler
|
13
|
+
# include Hooks::Core::ComponentAccess
|
14
|
+
#
|
15
|
+
# def process
|
16
|
+
# log.info("Processing request")
|
17
|
+
# stats.increment("requests.processed")
|
18
|
+
# failbot.report("Error occurred") if error?
|
19
|
+
# end
|
20
|
+
# end
|
21
|
+
#
|
22
|
+
# @example Usage in a class that needs class methods
|
23
|
+
# class MyValidator
|
24
|
+
# extend Hooks::Core::ComponentAccess
|
25
|
+
#
|
26
|
+
# def self.validate
|
27
|
+
# log.info("Validating request")
|
28
|
+
# stats.increment("requests.validated")
|
29
|
+
# end
|
30
|
+
# end
|
31
|
+
module ComponentAccess
|
32
|
+
# Short logger accessor
|
33
|
+
# @return [Hooks::Log] Logger instance for logging messages
|
34
|
+
#
|
35
|
+
# Provides a convenient way to log messages without needing
|
36
|
+
# to reference the full Hooks::Log namespace.
|
37
|
+
#
|
38
|
+
# @example Logging an error
|
39
|
+
# log.error("Something went wrong")
|
40
|
+
def log
|
41
|
+
Hooks::Log.instance
|
42
|
+
end
|
43
|
+
|
44
|
+
# Global stats component accessor
|
45
|
+
# @return [Hooks::Plugins::Instruments::Stats] Stats instance for metrics reporting
|
46
|
+
#
|
47
|
+
# Provides access to the global stats component for reporting metrics
|
48
|
+
# to services like DataDog, New Relic, etc.
|
49
|
+
#
|
50
|
+
# @example Recording a metric
|
51
|
+
# stats.increment("webhook.processed", { handler: "MyHandler" })
|
52
|
+
def stats
|
53
|
+
Hooks::Core::GlobalComponents.stats
|
54
|
+
end
|
55
|
+
|
56
|
+
# Global failbot component accessor
|
57
|
+
# @return [Hooks::Plugins::Instruments::Failbot] Failbot instance for error reporting
|
58
|
+
#
|
59
|
+
# Provides access to the global failbot component for reporting errors
|
60
|
+
# to services like Sentry, Rollbar, etc.
|
61
|
+
#
|
62
|
+
# @example Reporting an error
|
63
|
+
# failbot.report("Something went wrong", { context: "additional info" })
|
64
|
+
def failbot
|
65
|
+
Hooks::Core::GlobalComponents.failbot
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
@@ -24,23 +24,39 @@ module Hooks
|
|
24
24
|
normalize_headers: true
|
25
25
|
}.freeze
|
26
26
|
|
27
|
+
SILENCE_CONFIG_LOADER_MESSAGES = ENV.fetch(
|
28
|
+
"HOOKS_SILENCE_CONFIG_LOADER_MESSAGES", "false"
|
29
|
+
).downcase == "true".freeze
|
30
|
+
|
27
31
|
# Load and merge configuration from various sources
|
28
32
|
#
|
29
33
|
# @param config_path [String, Hash] Path to config file or config hash
|
30
34
|
# @return [Hash] Merged configuration
|
31
35
|
def self.load(config_path: nil)
|
32
36
|
config = DEFAULT_CONFIG.dup
|
37
|
+
overrides = []
|
33
38
|
|
34
39
|
# Load from file if path provided
|
35
40
|
if config_path.is_a?(String) && File.exist?(config_path)
|
36
41
|
file_config = load_config_file(config_path)
|
37
|
-
|
38
|
-
|
39
|
-
|
42
|
+
if file_config
|
43
|
+
overrides << "file config"
|
44
|
+
config.merge!(file_config)
|
45
|
+
end
|
40
46
|
end
|
41
47
|
|
42
|
-
# Override with environment variables
|
43
|
-
|
48
|
+
# Override with environment variables (before programmatic config)
|
49
|
+
env_config = load_env_config
|
50
|
+
if env_config.any?
|
51
|
+
overrides << "environment variables"
|
52
|
+
config.merge!(env_config)
|
53
|
+
end
|
54
|
+
|
55
|
+
# Programmatic config has highest priority
|
56
|
+
if config_path.is_a?(Hash)
|
57
|
+
overrides << "programmatic config"
|
58
|
+
config.merge!(config_path)
|
59
|
+
end
|
44
60
|
|
45
61
|
# Convert string keys to symbols for consistency
|
46
62
|
config = symbolize_keys(config)
|
@@ -51,6 +67,11 @@ module Hooks
|
|
51
67
|
config[:production] = false
|
52
68
|
end
|
53
69
|
|
70
|
+
# Log overrides if any were made
|
71
|
+
if overrides.any?
|
72
|
+
puts "INFO: Configuration overrides applied from: #{overrides.join(', ')}" unless SILENCE_CONFIG_LOADER_MESSAGES
|
73
|
+
end
|
74
|
+
|
54
75
|
return config
|
55
76
|
end
|
56
77
|
|
@@ -93,8 +114,9 @@ module Hooks
|
|
93
114
|
end
|
94
115
|
|
95
116
|
result
|
96
|
-
rescue =>
|
97
|
-
#
|
117
|
+
rescue => e
|
118
|
+
# Log this error with meaningful information
|
119
|
+
puts "ERROR: Failed to load config file '#{file_path}': #{e.message}" unless SILENCE_CONFIG_LOADER_MESSAGES
|
98
120
|
nil
|
99
121
|
end
|
100
122
|
|
@@ -105,8 +127,11 @@ module Hooks
|
|
105
127
|
env_config = {}
|
106
128
|
|
107
129
|
env_mappings = {
|
130
|
+
"HOOKS_HANDLER_DIR" => :handler_dir,
|
108
131
|
"HOOKS_HANDLER_PLUGIN_DIR" => :handler_plugin_dir,
|
109
132
|
"HOOKS_AUTH_PLUGIN_DIR" => :auth_plugin_dir,
|
133
|
+
"HOOKS_LIFECYCLE_PLUGIN_DIR" => :lifecycle_plugin_dir,
|
134
|
+
"HOOKS_INSTRUMENTS_PLUGIN_DIR" => :instruments_plugin_dir,
|
110
135
|
"HOOKS_LOG_LEVEL" => :log_level,
|
111
136
|
"HOOKS_REQUEST_LIMIT" => :request_limit,
|
112
137
|
"HOOKS_REQUEST_TIMEOUT" => :request_timeout,
|
@@ -114,17 +139,24 @@ module Hooks
|
|
114
139
|
"HOOKS_HEALTH_PATH" => :health_path,
|
115
140
|
"HOOKS_VERSION_PATH" => :version_path,
|
116
141
|
"HOOKS_ENVIRONMENT" => :environment,
|
117
|
-
"HOOKS_ENDPOINTS_DIR" => :endpoints_dir
|
142
|
+
"HOOKS_ENDPOINTS_DIR" => :endpoints_dir,
|
143
|
+
"HOOKS_USE_CATCHALL_ROUTE" => :use_catchall_route,
|
144
|
+
"HOOKS_SYMBOLIZE_PAYLOAD" => :symbolize_payload,
|
145
|
+
"HOOKS_NORMALIZE_HEADERS" => :normalize_headers,
|
146
|
+
"HOOKS_SOME_STRING_VAR" => :some_string_var # Added for test
|
118
147
|
}
|
119
148
|
|
120
149
|
env_mappings.each do |env_key, config_key|
|
121
150
|
value = ENV[env_key]
|
122
151
|
next unless value
|
123
152
|
|
124
|
-
# Convert
|
153
|
+
# Convert values to appropriate types
|
125
154
|
case config_key
|
126
155
|
when :request_limit, :request_timeout
|
127
156
|
env_config[config_key] = value.to_i
|
157
|
+
when :use_catchall_route, :symbolize_payload, :normalize_headers
|
158
|
+
# Convert string to boolean
|
159
|
+
env_config[config_key] = %w[true 1 yes on].include?(value.downcase)
|
128
160
|
else
|
129
161
|
env_config[config_key] = value
|
130
162
|
end
|
@@ -15,6 +15,8 @@ module Hooks
|
|
15
15
|
optional(:handler_dir).filled(:string) # For backward compatibility
|
16
16
|
optional(:handler_plugin_dir).filled(:string)
|
17
17
|
optional(:auth_plugin_dir).maybe(:string)
|
18
|
+
optional(:lifecycle_plugin_dir).maybe(:string)
|
19
|
+
optional(:instruments_plugin_dir).maybe(:string)
|
18
20
|
optional(:log_level).filled(:string, included_in?: %w[debug info warn error])
|
19
21
|
optional(:request_limit).filled(:integer, gt?: 0)
|
20
22
|
optional(:request_timeout).filled(:integer, gt?: 0)
|
@@ -32,6 +34,7 @@ module Hooks
|
|
32
34
|
ENDPOINT_CONFIG_SCHEMA = Dry::Schema.Params do
|
33
35
|
required(:path).filled(:string)
|
34
36
|
required(:handler).filled(:string)
|
37
|
+
optional(:method).filled(:string, included_in?: %w[get post put patch delete head options])
|
35
38
|
|
36
39
|
optional(:auth).hash do
|
37
40
|
required(:type).filled(:string)
|