hooks-ruby 0.0.2 → 0.0.3
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 +15 -3
- data/hooks.gemspec +0 -1
- data/lib/hooks/app/api.rb +55 -10
- data/lib/hooks/app/auth/auth.rb +14 -1
- data/lib/hooks/app/endpoints/catchall.rb +15 -3
- data/lib/hooks/app/endpoints/health.rb +1 -1
- data/lib/hooks/core/builder.rb +6 -0
- data/lib/hooks/core/config_validator.rb +2 -0
- data/lib/hooks/core/failbot.rb +50 -0
- data/lib/hooks/core/global_components.rb +51 -0
- data/lib/hooks/core/plugin_loader.rb +188 -2
- data/lib/hooks/core/stats.rb +54 -0
- data/lib/hooks/plugins/auth/base.rb +25 -0
- data/lib/hooks/plugins/handlers/base.rb +26 -0
- data/lib/hooks/plugins/instruments/failbot.rb +18 -0
- data/lib/hooks/plugins/instruments/failbot_base.rb +25 -0
- data/lib/hooks/plugins/instruments/stats.rb +18 -0
- data/lib/hooks/plugins/instruments/stats_base.rb +25 -0
- data/lib/hooks/plugins/lifecycle.rb +38 -0
- data/lib/hooks/utils/retry.rb +37 -0
- data/lib/hooks/version.rb +1 -1
- data/lib/hooks.rb +5 -0
- metadata +9 -21
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: cc9f23a8b74aef04b7a0016673a9dd835cef5f56eecb316e58b7cb3d19936f0d
|
4
|
+
data.tar.gz: '05290bf35da3f0aa73e5a0b9e0661d2986df03733862f6cd85c19248257f5b09'
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: a4462d77b184ac3be6b4a0f7ac4466dace8b5d146c722205ca7f97f455dc523de2168a71e2fe61b4a0946efdb7e9b8e4b61dc1e9940c8d14fdbaebebcb8fb497
|
7
|
+
data.tar.gz: bc2023bf7a08efd872e69603dd81dbbb2a4798deb4b4c98db37d65f8544ba57cfbdf36b82f2b48b33250d2429eb39946a71d65d7fbdf7c2d5e0a6b724e79a9cb
|
data/README.md
CHANGED
@@ -72,7 +72,7 @@ Here is a very high-level overview of how Hooks works:
|
|
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
|
@@ -286,10 +286,22 @@ 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
|
+
|
293
305
|
## Contributing 🤝
|
294
306
|
|
295
307
|
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,12 @@ 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
|
+
@server_start_time = Time.now
|
32
31
|
|
33
32
|
api_class = Class.new(Grape::API) do
|
34
33
|
content_type :json, "application/json"
|
@@ -51,20 +50,53 @@ module Hooks
|
|
51
50
|
|
52
51
|
post(full_path) do
|
53
52
|
request_id = uuid
|
53
|
+
start_time = Time.now
|
54
|
+
|
54
55
|
request_context = {
|
55
56
|
request_id:,
|
56
57
|
path: full_path,
|
57
58
|
handler: handler_class_name
|
58
59
|
}
|
59
60
|
|
61
|
+
# everything wrapped in the log context has access to the request context and includes it in log messages
|
62
|
+
# ex: Hooks::Log.info("message") will include request_id, path, handler, etc
|
60
63
|
Core::LogContext.with(request_context) do
|
61
64
|
begin
|
65
|
+
# Build Rack environment for lifecycle hooks
|
66
|
+
rack_env = {
|
67
|
+
"REQUEST_METHOD" => request.request_method,
|
68
|
+
"PATH_INFO" => request.path_info,
|
69
|
+
"QUERY_STRING" => request.query_string,
|
70
|
+
"HTTP_VERSION" => request.env["HTTP_VERSION"],
|
71
|
+
"REQUEST_URI" => request.url,
|
72
|
+
"SERVER_NAME" => request.env["SERVER_NAME"],
|
73
|
+
"SERVER_PORT" => request.env["SERVER_PORT"],
|
74
|
+
"CONTENT_TYPE" => request.content_type,
|
75
|
+
"CONTENT_LENGTH" => request.content_length,
|
76
|
+
"REMOTE_ADDR" => request.env["REMOTE_ADDR"],
|
77
|
+
"hooks.request_id" => request_id,
|
78
|
+
"hooks.handler" => handler_class_name,
|
79
|
+
"hooks.endpoint_config" => endpoint_config,
|
80
|
+
"hooks.start_time" => start_time.iso8601,
|
81
|
+
"hooks.full_path" => full_path
|
82
|
+
}
|
83
|
+
|
84
|
+
# Add HTTP headers to environment
|
85
|
+
headers.each do |key, value|
|
86
|
+
env_key = "HTTP_#{key.upcase.tr('-', '_')}"
|
87
|
+
rack_env[env_key] = value
|
88
|
+
end
|
89
|
+
|
90
|
+
# Call lifecycle hooks: on_request
|
91
|
+
Core::PluginLoader.lifecycle_plugins.each do |plugin|
|
92
|
+
plugin.on_request(rack_env)
|
93
|
+
end
|
94
|
+
|
62
95
|
enforce_request_limits(config)
|
63
96
|
request.body.rewind
|
64
97
|
raw_body = request.body.read
|
65
98
|
|
66
99
|
if endpoint_config[:auth]
|
67
|
-
log.info "validating request (id: #{request_id}, handler: #{handler_class_name})"
|
68
100
|
validate_auth!(raw_body, headers, endpoint_config, config)
|
69
101
|
end
|
70
102
|
|
@@ -78,16 +110,29 @@ module Hooks
|
|
78
110
|
config: endpoint_config
|
79
111
|
)
|
80
112
|
|
81
|
-
|
113
|
+
# Call lifecycle hooks: on_response
|
114
|
+
Core::PluginLoader.lifecycle_plugins.each do |plugin|
|
115
|
+
plugin.on_response(rack_env, response)
|
116
|
+
end
|
117
|
+
|
118
|
+
log.info("successfully processed webhook event with handler: #{handler_class_name}")
|
119
|
+
log.debug("processing duration: #{Time.now - start_time}s")
|
82
120
|
status 200
|
83
121
|
content_type "application/json"
|
84
|
-
|
85
|
-
rescue => e
|
86
|
-
|
122
|
+
response.to_json
|
123
|
+
rescue StandardError => e
|
124
|
+
# Call lifecycle hooks: on_error
|
125
|
+
if defined?(rack_env)
|
126
|
+
Core::PluginLoader.lifecycle_plugins.each do |plugin|
|
127
|
+
plugin.on_error(e, rack_env)
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
log.error("an error occuring during the processing of a webhook event - #{e.message}")
|
87
132
|
error_response = {
|
88
133
|
error: e.message,
|
89
134
|
code: determine_error_code(e),
|
90
|
-
request_id:
|
135
|
+
request_id:
|
91
136
|
}
|
92
137
|
error_response[:backtrace] = e.backtrace unless config[:production]
|
93
138
|
status error_response[:code]
|
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,9 +32,12 @@ 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
|
|
39
|
+
log.debug("validating auth for request with auth_class: #{auth_class.name}")
|
40
|
+
|
38
41
|
unless auth_class.valid?(
|
39
42
|
payload:,
|
40
43
|
headers:,
|
@@ -43,6 +46,16 @@ module Hooks
|
|
43
46
|
error!("authentication failed", 401)
|
44
47
|
end
|
45
48
|
end
|
49
|
+
|
50
|
+
private
|
51
|
+
|
52
|
+
# Short logger accessor for auth module
|
53
|
+
# @return [Hooks::Log] Logger instance
|
54
|
+
#
|
55
|
+
# Provides access to the application logger for authentication operations.
|
56
|
+
def log
|
57
|
+
Hooks::Log.instance
|
58
|
+
end
|
46
59
|
end
|
47
60
|
end
|
48
61
|
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
|
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
|
|
@@ -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)
|
@@ -0,0 +1,50 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Hooks
|
4
|
+
module Core
|
5
|
+
# Global failbot component for error reporting
|
6
|
+
#
|
7
|
+
# This is a stub implementation that does nothing by default.
|
8
|
+
# Users can replace this with their own implementation for services
|
9
|
+
# like Sentry, Rollbar, etc.
|
10
|
+
class Failbot
|
11
|
+
# Report an error or exception
|
12
|
+
#
|
13
|
+
# @param error_or_message [Exception, String] Exception object or error message
|
14
|
+
# @param context [Hash] Optional context information
|
15
|
+
# @return [void]
|
16
|
+
def report(error_or_message, context = {})
|
17
|
+
# Override in subclass for actual error reporting
|
18
|
+
end
|
19
|
+
|
20
|
+
# Report a critical error
|
21
|
+
#
|
22
|
+
# @param error_or_message [Exception, String] Exception object or error message
|
23
|
+
# @param context [Hash] Optional context information
|
24
|
+
# @return [void]
|
25
|
+
def critical(error_or_message, context = {})
|
26
|
+
# Override in subclass for actual error reporting
|
27
|
+
end
|
28
|
+
|
29
|
+
# Report a warning
|
30
|
+
#
|
31
|
+
# @param message [String] Warning message
|
32
|
+
# @param context [Hash] Optional context information
|
33
|
+
# @return [void]
|
34
|
+
def warning(message, context = {})
|
35
|
+
# Override in subclass for actual warning reporting
|
36
|
+
end
|
37
|
+
|
38
|
+
# Capture an exception during block execution
|
39
|
+
#
|
40
|
+
# @param context [Hash] Optional context information
|
41
|
+
# @return [Object] Return value of the block
|
42
|
+
def capture(context = {})
|
43
|
+
yield
|
44
|
+
rescue => e
|
45
|
+
report(e, context)
|
46
|
+
raise
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Hooks
|
4
|
+
module Core
|
5
|
+
# Global registry for shared components accessible throughout the application
|
6
|
+
class GlobalComponents
|
7
|
+
@test_stats = nil
|
8
|
+
@test_failbot = nil
|
9
|
+
|
10
|
+
# Get the global stats instance
|
11
|
+
# @return [Hooks::Plugins::Instruments::StatsBase] Stats instance for metrics reporting
|
12
|
+
def self.stats
|
13
|
+
@test_stats || PluginLoader.get_instrument_plugin(:stats)
|
14
|
+
end
|
15
|
+
|
16
|
+
# Get the global failbot instance
|
17
|
+
# @return [Hooks::Plugins::Instruments::FailbotBase] Failbot instance for error reporting
|
18
|
+
def self.failbot
|
19
|
+
@test_failbot || PluginLoader.get_instrument_plugin(:failbot)
|
20
|
+
end
|
21
|
+
|
22
|
+
# Set a custom stats instance (for testing)
|
23
|
+
# @param stats_instance [Object] Custom stats instance
|
24
|
+
def self.stats=(stats_instance)
|
25
|
+
@test_stats = stats_instance
|
26
|
+
end
|
27
|
+
|
28
|
+
# Set a custom failbot instance (for testing)
|
29
|
+
# @param failbot_instance [Object] Custom failbot instance
|
30
|
+
def self.failbot=(failbot_instance)
|
31
|
+
@test_failbot = failbot_instance
|
32
|
+
end
|
33
|
+
|
34
|
+
# Reset components to default instances (for testing)
|
35
|
+
#
|
36
|
+
# @return [void]
|
37
|
+
def self.reset
|
38
|
+
@test_stats = nil
|
39
|
+
@test_failbot = nil
|
40
|
+
# Clear and reload default instruments
|
41
|
+
PluginLoader.clear_plugins
|
42
|
+
require_relative "../plugins/instruments/stats"
|
43
|
+
require_relative "../plugins/instruments/failbot"
|
44
|
+
PluginLoader.instance_variable_set(:@instrument_plugins, {
|
45
|
+
stats: Hooks::Plugins::Instruments::Stats.new,
|
46
|
+
failbot: Hooks::Plugins::Instruments::Failbot.new
|
47
|
+
})
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -5,14 +5,16 @@ require_relative "../security"
|
|
5
5
|
|
6
6
|
module Hooks
|
7
7
|
module Core
|
8
|
-
# Loads and caches all plugins (auth + handlers) at boot time
|
8
|
+
# Loads and caches all plugins (auth + handlers + lifecycle + instruments) at boot time
|
9
9
|
class PluginLoader
|
10
10
|
# Class-level registries for loaded plugins
|
11
11
|
@auth_plugins = {}
|
12
12
|
@handler_plugins = {}
|
13
|
+
@lifecycle_plugins = []
|
14
|
+
@instrument_plugins = { stats: nil, failbot: nil }
|
13
15
|
|
14
16
|
class << self
|
15
|
-
attr_reader :auth_plugins, :handler_plugins
|
17
|
+
attr_reader :auth_plugins, :handler_plugins, :lifecycle_plugins, :instrument_plugins
|
16
18
|
|
17
19
|
# Load all plugins at boot time
|
18
20
|
#
|
@@ -22,6 +24,8 @@ module Hooks
|
|
22
24
|
# Clear existing registries
|
23
25
|
@auth_plugins = {}
|
24
26
|
@handler_plugins = {}
|
27
|
+
@lifecycle_plugins = []
|
28
|
+
@instrument_plugins = { stats: nil, failbot: nil }
|
25
29
|
|
26
30
|
# Load built-in plugins first
|
27
31
|
load_builtin_plugins
|
@@ -29,6 +33,11 @@ module Hooks
|
|
29
33
|
# Load custom plugins if directories are configured
|
30
34
|
load_custom_auth_plugins(config[:auth_plugin_dir]) if config[:auth_plugin_dir]
|
31
35
|
load_custom_handler_plugins(config[:handler_plugin_dir]) if config[:handler_plugin_dir]
|
36
|
+
load_custom_lifecycle_plugins(config[:lifecycle_plugin_dir]) if config[:lifecycle_plugin_dir]
|
37
|
+
load_custom_instrument_plugins(config[:instruments_plugin_dir]) if config[:instruments_plugin_dir]
|
38
|
+
|
39
|
+
# Load default instruments if no custom ones were loaded
|
40
|
+
load_default_instruments
|
32
41
|
|
33
42
|
# Log loaded plugins
|
34
43
|
log_loaded_plugins
|
@@ -65,12 +74,29 @@ module Hooks
|
|
65
74
|
plugin_class
|
66
75
|
end
|
67
76
|
|
77
|
+
# Get instrument plugin instance by type
|
78
|
+
#
|
79
|
+
# @param instrument_type [Symbol] Type of instrument (:stats or :failbot)
|
80
|
+
# @return [Object] The instrument plugin instance
|
81
|
+
# @raise [StandardError] if instrument not found
|
82
|
+
def get_instrument_plugin(instrument_type)
|
83
|
+
instrument_instance = @instrument_plugins[instrument_type]
|
84
|
+
|
85
|
+
unless instrument_instance
|
86
|
+
raise StandardError, "Instrument plugin '#{instrument_type}' not found"
|
87
|
+
end
|
88
|
+
|
89
|
+
instrument_instance
|
90
|
+
end
|
91
|
+
|
68
92
|
# Clear all loaded plugins (for testing purposes)
|
69
93
|
#
|
70
94
|
# @return [void]
|
71
95
|
def clear_plugins
|
72
96
|
@auth_plugins = {}
|
73
97
|
@handler_plugins = {}
|
98
|
+
@lifecycle_plugins = []
|
99
|
+
@instrument_plugins = { stats: nil, failbot: nil }
|
74
100
|
end
|
75
101
|
|
76
102
|
private
|
@@ -119,6 +145,38 @@ module Hooks
|
|
119
145
|
end
|
120
146
|
end
|
121
147
|
|
148
|
+
# Load custom lifecycle plugins from directory
|
149
|
+
#
|
150
|
+
# @param lifecycle_plugin_dir [String] Directory containing custom lifecycle plugins
|
151
|
+
# @return [void]
|
152
|
+
def load_custom_lifecycle_plugins(lifecycle_plugin_dir)
|
153
|
+
return unless lifecycle_plugin_dir && Dir.exist?(lifecycle_plugin_dir)
|
154
|
+
|
155
|
+
Dir.glob(File.join(lifecycle_plugin_dir, "*.rb")).sort.each do |file_path|
|
156
|
+
begin
|
157
|
+
load_custom_lifecycle_plugin(file_path, lifecycle_plugin_dir)
|
158
|
+
rescue => e
|
159
|
+
raise StandardError, "Failed to load lifecycle plugin from #{file_path}: #{e.message}"
|
160
|
+
end
|
161
|
+
end
|
162
|
+
end
|
163
|
+
|
164
|
+
# Load custom instrument plugins from directory
|
165
|
+
#
|
166
|
+
# @param instruments_plugin_dir [String] Directory containing custom instrument plugins
|
167
|
+
# @return [void]
|
168
|
+
def load_custom_instrument_plugins(instruments_plugin_dir)
|
169
|
+
return unless instruments_plugin_dir && Dir.exist?(instruments_plugin_dir)
|
170
|
+
|
171
|
+
Dir.glob(File.join(instruments_plugin_dir, "*.rb")).sort.each do |file_path|
|
172
|
+
begin
|
173
|
+
load_custom_instrument_plugin(file_path, instruments_plugin_dir)
|
174
|
+
rescue => e
|
175
|
+
raise StandardError, "Failed to load instrument plugin from #{file_path}: #{e.message}"
|
176
|
+
end
|
177
|
+
end
|
178
|
+
end
|
179
|
+
|
122
180
|
# Load a single custom auth plugin file
|
123
181
|
#
|
124
182
|
# @param file_path [String] Path to the auth plugin file
|
@@ -189,6 +247,90 @@ module Hooks
|
|
189
247
|
@handler_plugins[class_name] = handler_class
|
190
248
|
end
|
191
249
|
|
250
|
+
# Load a single custom lifecycle plugin file
|
251
|
+
#
|
252
|
+
# @param file_path [String] Path to the lifecycle plugin file
|
253
|
+
# @param lifecycle_plugin_dir [String] Base directory for lifecycle plugins
|
254
|
+
# @return [void]
|
255
|
+
def load_custom_lifecycle_plugin(file_path, lifecycle_plugin_dir)
|
256
|
+
# Security: Ensure the file path doesn't escape the lifecycle plugin directory
|
257
|
+
normalized_lifecycle_dir = Pathname.new(File.expand_path(lifecycle_plugin_dir))
|
258
|
+
normalized_file_path = Pathname.new(File.expand_path(file_path))
|
259
|
+
unless normalized_file_path.descend.any? { |path| path == normalized_lifecycle_dir }
|
260
|
+
raise SecurityError, "Lifecycle plugin path outside of lifecycle plugin directory: #{file_path}"
|
261
|
+
end
|
262
|
+
|
263
|
+
# Extract class name from file (e.g., logging_lifecycle.rb -> LoggingLifecycle)
|
264
|
+
file_name = File.basename(file_path, ".rb")
|
265
|
+
class_name = file_name.split("_").map(&:capitalize).join("")
|
266
|
+
|
267
|
+
# Security: Validate class name
|
268
|
+
unless valid_lifecycle_class_name?(class_name)
|
269
|
+
raise StandardError, "Invalid lifecycle plugin class name: #{class_name}"
|
270
|
+
end
|
271
|
+
|
272
|
+
# Load the file
|
273
|
+
require file_path
|
274
|
+
|
275
|
+
# Get the class and validate it
|
276
|
+
lifecycle_class = Object.const_get(class_name)
|
277
|
+
unless lifecycle_class < Hooks::Plugins::Lifecycle
|
278
|
+
raise StandardError, "Lifecycle plugin class must inherit from Hooks::Plugins::Lifecycle: #{class_name}"
|
279
|
+
end
|
280
|
+
|
281
|
+
# Register the plugin instance
|
282
|
+
@lifecycle_plugins << lifecycle_class.new
|
283
|
+
end
|
284
|
+
|
285
|
+
# Load a single custom instrument plugin file
|
286
|
+
#
|
287
|
+
# @param file_path [String] Path to the instrument plugin file
|
288
|
+
# @param instruments_plugin_dir [String] Base directory for instrument plugins
|
289
|
+
# @return [void]
|
290
|
+
def load_custom_instrument_plugin(file_path, instruments_plugin_dir)
|
291
|
+
# Security: Ensure the file path doesn't escape the instruments plugin directory
|
292
|
+
normalized_instruments_dir = Pathname.new(File.expand_path(instruments_plugin_dir))
|
293
|
+
normalized_file_path = Pathname.new(File.expand_path(file_path))
|
294
|
+
unless normalized_file_path.descend.any? { |path| path == normalized_instruments_dir }
|
295
|
+
raise SecurityError, "Instrument plugin path outside of instruments plugin directory: #{file_path}"
|
296
|
+
end
|
297
|
+
|
298
|
+
# Extract class name from file (e.g., custom_stats.rb -> CustomStats)
|
299
|
+
file_name = File.basename(file_path, ".rb")
|
300
|
+
class_name = file_name.split("_").map(&:capitalize).join("")
|
301
|
+
|
302
|
+
# Security: Validate class name
|
303
|
+
unless valid_instrument_class_name?(class_name)
|
304
|
+
raise StandardError, "Invalid instrument plugin class name: #{class_name}"
|
305
|
+
end
|
306
|
+
|
307
|
+
# Load the file
|
308
|
+
require file_path
|
309
|
+
|
310
|
+
# Get the class and validate it
|
311
|
+
instrument_class = Object.const_get(class_name)
|
312
|
+
|
313
|
+
# Determine instrument type based on inheritance
|
314
|
+
if instrument_class < Hooks::Plugins::Instruments::StatsBase
|
315
|
+
@instrument_plugins[:stats] = instrument_class.new
|
316
|
+
elsif instrument_class < Hooks::Plugins::Instruments::FailbotBase
|
317
|
+
@instrument_plugins[:failbot] = instrument_class.new
|
318
|
+
else
|
319
|
+
raise StandardError, "Instrument plugin class must inherit from StatsBase or FailbotBase: #{class_name}"
|
320
|
+
end
|
321
|
+
end
|
322
|
+
|
323
|
+
# Load default instrument implementations if no custom ones were loaded
|
324
|
+
#
|
325
|
+
# @return [void]
|
326
|
+
def load_default_instruments
|
327
|
+
require_relative "../plugins/instruments/stats"
|
328
|
+
require_relative "../plugins/instruments/failbot"
|
329
|
+
|
330
|
+
@instrument_plugins[:stats] ||= Hooks::Plugins::Instruments::Stats.new
|
331
|
+
@instrument_plugins[:failbot] ||= Hooks::Plugins::Instruments::Failbot.new
|
332
|
+
end
|
333
|
+
|
192
334
|
# Log summary of loaded plugins
|
193
335
|
#
|
194
336
|
# @return [void]
|
@@ -201,6 +343,8 @@ module Hooks
|
|
201
343
|
|
202
344
|
log.info "Loaded #{@auth_plugins.size} auth plugins: #{@auth_plugins.keys.join(', ')}"
|
203
345
|
log.info "Loaded #{@handler_plugins.size} handler plugins: #{@handler_plugins.keys.join(', ')}"
|
346
|
+
log.info "Loaded #{@lifecycle_plugins.size} lifecycle plugins"
|
347
|
+
log.info "Loaded instruments: #{@instrument_plugins.keys.select { |k| @instrument_plugins[k] }.join(', ')}"
|
204
348
|
end
|
205
349
|
|
206
350
|
# Validate that an auth plugin class name is safe to load
|
@@ -244,6 +388,48 @@ module Hooks
|
|
244
388
|
|
245
389
|
true
|
246
390
|
end
|
391
|
+
|
392
|
+
# Validate that a lifecycle plugin class name is safe to load
|
393
|
+
#
|
394
|
+
# @param class_name [String] The class name to validate
|
395
|
+
# @return [Boolean] true if the class name is safe, false otherwise
|
396
|
+
def valid_lifecycle_class_name?(class_name)
|
397
|
+
# Must be a string
|
398
|
+
return false unless class_name.is_a?(String)
|
399
|
+
|
400
|
+
# Must not be empty or only whitespace
|
401
|
+
return false if class_name.strip.empty?
|
402
|
+
|
403
|
+
# Must match a safe pattern: alphanumeric + underscore, starting with uppercase
|
404
|
+
# Examples: LoggingLifecycle, MetricsLifecycle, CustomLifecycle
|
405
|
+
return false unless class_name.match?(/\A[A-Z][a-zA-Z0-9_]*\z/)
|
406
|
+
|
407
|
+
# Must not be a system/built-in class name
|
408
|
+
return false if Hooks::Security::DANGEROUS_CLASSES.include?(class_name)
|
409
|
+
|
410
|
+
true
|
411
|
+
end
|
412
|
+
|
413
|
+
# Validate that an instrument plugin class name is safe to load
|
414
|
+
#
|
415
|
+
# @param class_name [String] The class name to validate
|
416
|
+
# @return [Boolean] true if the class name is safe, false otherwise
|
417
|
+
def valid_instrument_class_name?(class_name)
|
418
|
+
# Must be a string
|
419
|
+
return false unless class_name.is_a?(String)
|
420
|
+
|
421
|
+
# Must not be empty or only whitespace
|
422
|
+
return false if class_name.strip.empty?
|
423
|
+
|
424
|
+
# Must match a safe pattern: alphanumeric + underscore, starting with uppercase
|
425
|
+
# Examples: CustomStats, CustomFailbot, DatadogStats
|
426
|
+
return false unless class_name.match?(/\A[A-Z][a-zA-Z0-9_]*\z/)
|
427
|
+
|
428
|
+
# Must not be a system/built-in class name
|
429
|
+
return false if Hooks::Security::DANGEROUS_CLASSES.include?(class_name)
|
430
|
+
|
431
|
+
true
|
432
|
+
end
|
247
433
|
end
|
248
434
|
end
|
249
435
|
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Hooks
|
4
|
+
module Core
|
5
|
+
# Global stats component for metrics reporting
|
6
|
+
#
|
7
|
+
# This is a stub implementation that does nothing by default.
|
8
|
+
# Users can replace this with their own implementation for services
|
9
|
+
# like DataDog, New Relic, etc.
|
10
|
+
class Stats
|
11
|
+
# Record a metric
|
12
|
+
#
|
13
|
+
# @param metric_name [String] Name of the metric
|
14
|
+
# @param value [Numeric] Value to record
|
15
|
+
# @param tags [Hash] Optional tags/labels for the metric
|
16
|
+
# @return [void]
|
17
|
+
def record(metric_name, value, tags = {})
|
18
|
+
# Override in subclass for actual metrics reporting
|
19
|
+
end
|
20
|
+
|
21
|
+
# Increment a counter
|
22
|
+
#
|
23
|
+
# @param metric_name [String] Name of the counter
|
24
|
+
# @param tags [Hash] Optional tags/labels for the metric
|
25
|
+
# @return [void]
|
26
|
+
def increment(metric_name, tags = {})
|
27
|
+
# Override in subclass for actual metrics reporting
|
28
|
+
end
|
29
|
+
|
30
|
+
# Record a timing metric
|
31
|
+
#
|
32
|
+
# @param metric_name [String] Name of the timing metric
|
33
|
+
# @param duration [Numeric] Duration in seconds
|
34
|
+
# @param tags [Hash] Optional tags/labels for the metric
|
35
|
+
# @return [void]
|
36
|
+
def timing(metric_name, duration, tags = {})
|
37
|
+
# Override in subclass for actual metrics reporting
|
38
|
+
end
|
39
|
+
|
40
|
+
# Measure execution time of a block
|
41
|
+
#
|
42
|
+
# @param metric_name [String] Name of the timing metric
|
43
|
+
# @param tags [Hash] Optional tags/labels for the metric
|
44
|
+
# @return [Object] Return value of the block
|
45
|
+
def measure(metric_name, tags = {})
|
46
|
+
start_time = Time.now
|
47
|
+
result = yield
|
48
|
+
duration = Time.now - start_time
|
49
|
+
timing(metric_name, duration, tags)
|
50
|
+
result
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
@@ -2,6 +2,7 @@
|
|
2
2
|
|
3
3
|
require "rack/utils"
|
4
4
|
require_relative "../../core/log"
|
5
|
+
require_relative "../../core/global_components"
|
5
6
|
|
6
7
|
module Hooks
|
7
8
|
module Plugins
|
@@ -33,6 +34,30 @@ module Hooks
|
|
33
34
|
Hooks::Log.instance
|
34
35
|
end
|
35
36
|
|
37
|
+
# Global stats component accessor
|
38
|
+
# @return [Hooks::Core::Stats] Stats instance for metrics reporting
|
39
|
+
#
|
40
|
+
# Provides access to the global stats component for reporting metrics
|
41
|
+
# to services like DataDog, New Relic, etc.
|
42
|
+
#
|
43
|
+
# @example Recording a metric in an inherited class
|
44
|
+
# stats.increment("auth.validation", { plugin: "hmac" })
|
45
|
+
def self.stats
|
46
|
+
Hooks::Core::GlobalComponents.stats
|
47
|
+
end
|
48
|
+
|
49
|
+
# Global failbot component accessor
|
50
|
+
# @return [Hooks::Core::Failbot] Failbot instance for error reporting
|
51
|
+
#
|
52
|
+
# Provides access to the global failbot component for reporting errors
|
53
|
+
# to services like Sentry, Rollbar, etc.
|
54
|
+
#
|
55
|
+
# @example Reporting an error in an inherited class
|
56
|
+
# failbot.report("Auth validation failed", { plugin: "hmac" })
|
57
|
+
def self.failbot
|
58
|
+
Hooks::Core::GlobalComponents.failbot
|
59
|
+
end
|
60
|
+
|
36
61
|
# Retrieve the secret from the environment variable based on the key set in the configuration
|
37
62
|
#
|
38
63
|
# Note: This method is intended to be used by subclasses
|
@@ -1,5 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require_relative "../../core/global_components"
|
4
|
+
|
3
5
|
module Hooks
|
4
6
|
module Plugins
|
5
7
|
module Handlers
|
@@ -29,6 +31,30 @@ module Hooks
|
|
29
31
|
def log
|
30
32
|
Hooks::Log.instance
|
31
33
|
end
|
34
|
+
|
35
|
+
# Global stats component accessor
|
36
|
+
# @return [Hooks::Core::Stats] Stats instance for metrics reporting
|
37
|
+
#
|
38
|
+
# Provides access to the global stats component for reporting metrics
|
39
|
+
# to services like DataDog, New Relic, etc.
|
40
|
+
#
|
41
|
+
# @example Recording a metric in an inherited class
|
42
|
+
# stats.increment("webhook.processed", { handler: "MyHandler" })
|
43
|
+
def stats
|
44
|
+
Hooks::Core::GlobalComponents.stats
|
45
|
+
end
|
46
|
+
|
47
|
+
# Global failbot component accessor
|
48
|
+
# @return [Hooks::Core::Failbot] Failbot instance for error reporting
|
49
|
+
#
|
50
|
+
# Provides access to the global failbot component for reporting errors
|
51
|
+
# to services like Sentry, Rollbar, etc.
|
52
|
+
#
|
53
|
+
# @example Reporting an error in an inherited class
|
54
|
+
# failbot.report("Something went wrong", { handler: "MyHandler" })
|
55
|
+
def failbot
|
56
|
+
Hooks::Core::GlobalComponents.failbot
|
57
|
+
end
|
32
58
|
end
|
33
59
|
end
|
34
60
|
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "failbot_base"
|
4
|
+
|
5
|
+
module Hooks
|
6
|
+
module Plugins
|
7
|
+
module Instruments
|
8
|
+
# Default failbot instrument implementation
|
9
|
+
#
|
10
|
+
# This is a stub implementation that does nothing by default.
|
11
|
+
# Users can replace this with their own implementation for services
|
12
|
+
# like Sentry, Rollbar, etc.
|
13
|
+
class Failbot < FailbotBase
|
14
|
+
# Inherit from FailbotBase to provide a default implementation of the failbot instrument.
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Hooks
|
4
|
+
module Plugins
|
5
|
+
module Instruments
|
6
|
+
# Base class for all failbot instrument plugins
|
7
|
+
#
|
8
|
+
# All custom failbot implementations must inherit from this class and implement
|
9
|
+
# the required methods for error reporting.
|
10
|
+
class FailbotBase
|
11
|
+
# Short logger accessor for all subclasses
|
12
|
+
# @return [Hooks::Log] Logger instance
|
13
|
+
#
|
14
|
+
# Provides a convenient way for instruments to log messages without needing
|
15
|
+
# to reference the full Hooks::Log namespace.
|
16
|
+
#
|
17
|
+
# @example Logging debug info in an inherited class
|
18
|
+
# log.debug("Sending error to external service")
|
19
|
+
def log
|
20
|
+
Hooks::Log.instance
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "stats_base"
|
4
|
+
|
5
|
+
module Hooks
|
6
|
+
module Plugins
|
7
|
+
module Instruments
|
8
|
+
# Default stats instrument implementation
|
9
|
+
#
|
10
|
+
# This is a stub implementation that does nothing by default.
|
11
|
+
# Users can replace this with their own implementation for services
|
12
|
+
# like DataDog, New Relic, etc.
|
13
|
+
class Stats < StatsBase
|
14
|
+
# Inherit from StatsBase to provide a default implementation of the stats instrument.
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Hooks
|
4
|
+
module Plugins
|
5
|
+
module Instruments
|
6
|
+
# Base class for all stats instrument plugins
|
7
|
+
#
|
8
|
+
# All custom stats implementations must inherit from this class and implement
|
9
|
+
# the required methods for metrics reporting.
|
10
|
+
class StatsBase
|
11
|
+
# Short logger accessor for all subclasses
|
12
|
+
# @return [Hooks::Log] Logger instance
|
13
|
+
#
|
14
|
+
# Provides a convenient way for instruments to log messages without needing
|
15
|
+
# to reference the full Hooks::Log namespace.
|
16
|
+
#
|
17
|
+
# @example Logging an error in an inherited class
|
18
|
+
# log.error("Failed to send metric to external service")
|
19
|
+
def log
|
20
|
+
Hooks::Log.instance
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -1,5 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require_relative "../core/global_components"
|
4
|
+
|
3
5
|
module Hooks
|
4
6
|
module Plugins
|
5
7
|
# Base class for global lifecycle plugins
|
@@ -28,6 +30,42 @@ module Hooks
|
|
28
30
|
def on_error(exception, env)
|
29
31
|
# Override in subclass for error handling logic
|
30
32
|
end
|
33
|
+
|
34
|
+
# Short logger accessor for all subclasses
|
35
|
+
# @return [Hooks::Log] Logger instance
|
36
|
+
#
|
37
|
+
# Provides a convenient way for lifecycle plugins to log messages without needing
|
38
|
+
# to reference the full Hooks::Log namespace.
|
39
|
+
#
|
40
|
+
# @example Logging an error in an inherited class
|
41
|
+
# log.error("oh no an error occured")
|
42
|
+
def log
|
43
|
+
Hooks::Log.instance
|
44
|
+
end
|
45
|
+
|
46
|
+
# Global stats component accessor
|
47
|
+
# @return [Hooks::Core::Stats] Stats instance for metrics reporting
|
48
|
+
#
|
49
|
+
# Provides access to the global stats component for reporting metrics
|
50
|
+
# to services like DataDog, New Relic, etc.
|
51
|
+
#
|
52
|
+
# @example Recording a metric in an inherited class
|
53
|
+
# stats.increment("lifecycle.request_processed")
|
54
|
+
def stats
|
55
|
+
Hooks::Core::GlobalComponents.stats
|
56
|
+
end
|
57
|
+
|
58
|
+
# Global failbot component accessor
|
59
|
+
# @return [Hooks::Core::Failbot] Failbot instance for error reporting
|
60
|
+
#
|
61
|
+
# Provides access to the global failbot component for reporting errors
|
62
|
+
# to services like Sentry, Rollbar, etc.
|
63
|
+
#
|
64
|
+
# @example Reporting an error in an inherited class
|
65
|
+
# failbot.report("Lifecycle hook failed")
|
66
|
+
def failbot
|
67
|
+
Hooks::Core::GlobalComponents.failbot
|
68
|
+
end
|
31
69
|
end
|
32
70
|
end
|
33
71
|
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "retryable"
|
4
|
+
|
5
|
+
# Utility module for retry functionality
|
6
|
+
module Retry
|
7
|
+
# This method should be called as early as possible in the startup of your application
|
8
|
+
# It sets up the Retryable gem with custom contexts and passes through a few options
|
9
|
+
# Should the number of retries be reached without success, the last exception will be raised
|
10
|
+
#
|
11
|
+
# @param log [Logger] The logger to use for retryable logging
|
12
|
+
# @raise [ArgumentError] If no logger is provided
|
13
|
+
# @return [void]
|
14
|
+
def self.setup!(log: nil, log_retries: ENV.fetch("RETRY_LOG_RETRIES", "true") == "true")
|
15
|
+
raise ArgumentError, "a logger must be provided" if log.nil?
|
16
|
+
|
17
|
+
log_method = lambda do |retries, exception|
|
18
|
+
# :nocov:
|
19
|
+
if log_retries
|
20
|
+
log.debug("[retry ##{retries}] #{exception.class}: #{exception.message} - #{exception.backtrace.join("\n")}")
|
21
|
+
end
|
22
|
+
# :nocov:
|
23
|
+
end
|
24
|
+
|
25
|
+
######## Retryable Configuration ########
|
26
|
+
# All defaults available here:
|
27
|
+
# https://github.com/nfedyashev/retryable/blob/6a04027e61607de559e15e48f281f3ccaa9750e8/lib/retryable/configuration.rb#L22-L33
|
28
|
+
Retryable.configure do |config|
|
29
|
+
config.contexts[:default] = {
|
30
|
+
on: [StandardError],
|
31
|
+
sleep: ENV.fetch("DEFAULT_RETRY_SLEEP", "1").to_i,
|
32
|
+
tries: ENV.fetch("DEFAULT_RETRY_TRIES", "10").to_i,
|
33
|
+
log_method:
|
34
|
+
}
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
data/lib/hooks/version.rb
CHANGED
data/lib/hooks.rb
CHANGED
@@ -3,6 +3,11 @@
|
|
3
3
|
require_relative "hooks/version"
|
4
4
|
require_relative "hooks/core/builder"
|
5
5
|
|
6
|
+
# Load all core components
|
7
|
+
Dir[File.join(__dir__, "hooks/core/**/*.rb")].sort.each do |file|
|
8
|
+
require file
|
9
|
+
end
|
10
|
+
|
6
11
|
# Load all plugins (auth plugins, handler plugins, lifecycle hooks, etc.)
|
7
12
|
Dir[File.join(__dir__, "hooks/plugins/**/*.rb")].sort.each do |file|
|
8
13
|
require file
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: hooks-ruby
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.3
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- github
|
@@ -78,26 +78,6 @@ dependencies:
|
|
78
78
|
- - "~>"
|
79
79
|
- !ruby/object:Gem::Version
|
80
80
|
version: '2.3'
|
81
|
-
- !ruby/object:Gem::Dependency
|
82
|
-
name: grape-swagger
|
83
|
-
requirement: !ruby/object:Gem::Requirement
|
84
|
-
requirements:
|
85
|
-
- - "~>"
|
86
|
-
- !ruby/object:Gem::Version
|
87
|
-
version: '2.1'
|
88
|
-
- - ">="
|
89
|
-
- !ruby/object:Gem::Version
|
90
|
-
version: 2.1.2
|
91
|
-
type: :runtime
|
92
|
-
prerelease: false
|
93
|
-
version_requirements: !ruby/object:Gem::Requirement
|
94
|
-
requirements:
|
95
|
-
- - "~>"
|
96
|
-
- !ruby/object:Gem::Version
|
97
|
-
version: '2.1'
|
98
|
-
- - ">="
|
99
|
-
- !ruby/object:Gem::Version
|
100
|
-
version: 2.1.2
|
101
81
|
- !ruby/object:Gem::Dependency
|
102
82
|
name: puma
|
103
83
|
requirement: !ruby/object:Gem::Requirement
|
@@ -152,17 +132,25 @@ files:
|
|
152
132
|
- lib/hooks/core/builder.rb
|
153
133
|
- lib/hooks/core/config_loader.rb
|
154
134
|
- lib/hooks/core/config_validator.rb
|
135
|
+
- lib/hooks/core/failbot.rb
|
136
|
+
- lib/hooks/core/global_components.rb
|
155
137
|
- lib/hooks/core/log.rb
|
156
138
|
- lib/hooks/core/logger_factory.rb
|
157
139
|
- lib/hooks/core/plugin_loader.rb
|
140
|
+
- lib/hooks/core/stats.rb
|
158
141
|
- lib/hooks/plugins/auth/base.rb
|
159
142
|
- lib/hooks/plugins/auth/hmac.rb
|
160
143
|
- lib/hooks/plugins/auth/shared_secret.rb
|
161
144
|
- lib/hooks/plugins/handlers/base.rb
|
162
145
|
- lib/hooks/plugins/handlers/default.rb
|
146
|
+
- lib/hooks/plugins/instruments/failbot.rb
|
147
|
+
- lib/hooks/plugins/instruments/failbot_base.rb
|
148
|
+
- lib/hooks/plugins/instruments/stats.rb
|
149
|
+
- lib/hooks/plugins/instruments/stats_base.rb
|
163
150
|
- lib/hooks/plugins/lifecycle.rb
|
164
151
|
- lib/hooks/security.rb
|
165
152
|
- lib/hooks/utils/normalize.rb
|
153
|
+
- lib/hooks/utils/retry.rb
|
166
154
|
- lib/hooks/version.rb
|
167
155
|
homepage: https://github.com/github/hooks
|
168
156
|
licenses:
|