tpt-rails 1.5.1 → 1.6.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0aac14c9d173af50335042c50b4831a94aae59d754e93ff5c9e7221094516679
4
- data.tar.gz: d76804b0ecb5f42a93d62cb502bbfee0c18849b50154b7702e3bfd12be27ba1d
3
+ metadata.gz: '0395183e764d77487d191133045533748b1120aa919bd2904d19e923c58b5c1d'
4
+ data.tar.gz: f8591848d97fb4324aed4318183198db27ccfcff116ad43f806cd2322aadcb57
5
5
  SHA512:
6
- metadata.gz: 1b11d48d48cccc11b32aeff9fa8500d4414fbbb68235659bf0d6a6059cef79f213c2dcee6e1c86481473879f99f41621e56d017b36ce3d400aba7548c14cbd07
7
- data.tar.gz: 5516c6cf9f62916825d8558d82db9b9131cafe295c6cf0a80f81d0d0d8aa83d645cfa92681f51811db9dccf7e2702747ac16cb899dd7b512b631e3a3b0d032ad
6
+ metadata.gz: 3093e622d8895edf09b3d2ee025c1356e89bdd6d7ea95dd623e5f870e8d2adab915bc4f279c548f24d0636ee902825c163b1ea61f7ce21ef5a66c970dc0c55f8
7
+ data.tar.gz: 11459ad795f7603c051f7c509ff475bdfd21bd52ce37b26ad4792da3a833fdea0a4294f614965cd28a0716f51ed79039b5fe39ebde21458e2901595e6f0ca1e7
data/README.md CHANGED
@@ -22,11 +22,13 @@ can configure this gem and enable optional features in that file.
22
22
  See the documentation in [lib/tpt/rails.rb](lib/tpt/rails.rb).
23
23
 
24
24
  For Kubernetes configuration:
25
+
25
26
  ```sh
26
27
  rails generate tpt:rails:kubernetes
27
28
  ```
28
29
 
29
30
  For CI/CD configuration:
31
+
30
32
  ```sh
31
33
  rails generate tpt:rails:continuous_integration
32
34
  ```
@@ -45,6 +47,8 @@ This gem provides the following features:
45
47
  - Adds an error test at `/internal/error-test` that reports an error and emits a metric
46
48
  - Kubernetes/Helm configuration
47
49
  - CI/CD configuration
50
+ - EventBridge integration
51
+ - Adds the aws and aws eventbridge gems, and the EventBridgePublisher class
48
52
 
49
53
  See below for more details.
50
54
 
@@ -60,6 +64,35 @@ Tpt::Rails.statsd.increment('foo', tags: ['a:b', 'c:d'])
60
64
 
61
65
  See the documentation in [lib/tpt/rails.rb](lib/tpt/rails.rb).
62
66
 
67
+ ### EventBridge
68
+
69
+ `Tpt::Rails::EventBridgePublisher` is the class used to emit events to EventBridge (in batches). To use:
70
+
71
+ 1. Add EventBridge to your gemfile:
72
+
73
+ ```ruby
74
+ gem "aws-sdk-eventbridge", "~> 1.3.0"
75
+ ```
76
+
77
+ 2. In an initializer, pass in the `Datadog::Statsd` object you're using to the class method ::set_metrics.
78
+ (Failure to do so will result in an error.) When testing, you can pass in a local Statsd, eg:
79
+
80
+ ```ruby
81
+ Tpt::Rails::EventBridgePublisher.set_metrics(
82
+ Datadog::Statsd.new('localhost', 8125)
83
+ )
84
+ ```
85
+
86
+ 3. By default (#new with no arguments) EventBridgePublisher will try to use the following environment
87
+ variables: `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`, `TPT_EVENT_SOURCE`, `TPT_SHARED_EVENT_BUS_NAME`.
88
+
89
+ Additional configuration can be set as part of the constructor;
90
+ [see event_bridge_publisher.rb implementation](https://github.com/TeachersPayTeachers/tpt-rails/blob/ad768d3bac0c8bd28e60a48a89aed36f01c4e17b/app/models/tpt/rails/event_bridge_publisher.rb) for more details.
91
+
92
+ 4. Create events by instantiating or inheriting from `Tpt::Rails::ApplicationEvent`. Event types are derived from the text before
93
+ "Event", so ResourceEvent has the event_type "Resource". Pass in an event or an array of events to EventBridgePublisher#publish
94
+ and they will be published immediately. Large arrays will be batched (default 10 per batch).
95
+
63
96
  ### Error reporting (e.g. Bugsnag/Rollbar)
64
97
 
65
98
  If you configure `rollbar_access_token` & `rollbar_enabled`, or `bugsnag_api_key` then your project
@@ -81,10 +114,12 @@ Tpt::Rails provides the following endpoint:
81
114
  /internal/health-check
82
115
 
83
116
  Two checks are added automatically:
117
+
84
118
  - An ActiveRecord database check is performed if ActiveRecord is used in the app.
85
119
  - A Redis check is performed if Redis is used in the app.
86
120
 
87
121
  You may add more health checks as follows:
122
+
88
123
  ```ruby
89
124
  config.health_check(:foo) { 1 == 1 }
90
125
  ```
@@ -132,8 +167,8 @@ gem 'tpt-rails', path: '/your/local/path/to/tpt/rails'
132
167
  First ensure you have permissions to publish to rubygems.org.
133
168
 
134
169
  Once you've merged, check out the `master` branch and execute:
170
+
135
171
  ```sh
136
172
  bundle exec gem bump --push --tag --version patch # patch/minor/major/X.X.X
137
173
  bundle exec gem release
138
174
  ```
139
-
@@ -0,0 +1,30 @@
1
+ module Tpt::Rails
2
+ class ApplicationEvent
3
+ include ActiveModel::Serializers::JSON
4
+
5
+ def initialize(data, metadata: {})
6
+ @data = data || {}
7
+ @metadata = metadata
8
+ end
9
+
10
+ def event_type
11
+ self.class.name.demodulize.gsub(/Event$/, '')
12
+ end
13
+
14
+ def ==(other)
15
+ @data == other.data
16
+ end
17
+
18
+ def attributes
19
+ data.merge(metadata: metadata)
20
+ end
21
+
22
+ protected
23
+ attr_reader :data, :metadata
24
+
25
+ private
26
+ def read_attribute_for_serialization(attr)
27
+ attributes[attr]
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,133 @@
1
+ module Tpt::Rails
2
+ class EventBridgePublisher
3
+ attr_accessor :batch_size
4
+
5
+ def initialize(
6
+ event_bus_name: default_event_bus_name,
7
+ event_source: default_event_source,
8
+ batch_size: 10,
9
+ client_options: {}
10
+ )
11
+ @event_bus_name = event_bus_name
12
+ @event_source = event_source
13
+ @batch_size = batch_size
14
+ @client = make_client(client_options)
15
+ end
16
+
17
+ def publish(events)
18
+ events = [events] unless events.is_a?(Array)
19
+
20
+ logger.debug "Publishing #{events.length} events to EventBridge in #{events.length/@batch_size} batch(es)"
21
+
22
+ metrics.time('tpt.event_bridge.publish') do
23
+ events.each_slice(@batch_size) do |batch|
24
+ publish_batch(batch)
25
+ end
26
+ end
27
+
28
+ events
29
+ end
30
+
31
+ private
32
+ attr_reader :client, :event_bus_name, :event_source
33
+
34
+ def publish_batch(events)
35
+ metrics.time('tpt.event_bridge.publish_batch') do
36
+ wrapped_events = wrap_events(events)
37
+ logger.debug({ msg: "Publishing batch of #{events.length} events to EventBridge", events: wrapped_events })
38
+ res = client.put_events(entries: wrapped_events)
39
+ handle_response(res, events)
40
+ end
41
+ rescue StandardError => e
42
+ logger.error({
43
+ msg: 'Failed to publish events',
44
+ events: events,
45
+ error: e
46
+ })
47
+ metrics.increment('tpt.event_bridge.publish_failure', by: events.length, tags: ["error_code:publish_batch_failure"])
48
+
49
+ # re-reraise the error for users to handle
50
+ raise e
51
+ end
52
+
53
+ def wrap_events(events)
54
+ events.map do |event|
55
+ {
56
+ source: event_source,
57
+ resources: [],
58
+ event_bus_name: event_bus_name,
59
+ detail: deep_camelize(event.as_json).to_json,
60
+ detail_type: event.event_type,
61
+ }
62
+ end
63
+ end
64
+
65
+ def handle_response(res, events)
66
+ res.data.entries.each_with_index do |entry, index|
67
+ if entry.error_code.present?
68
+ metrics.increment('tpt.event_bridge.publish_failure', tags: ["error_code:#{entry.error_code}"])
69
+
70
+ logger.error({
71
+ msg: "Error publishing event to EventBridge",
72
+ error_code: entry.error_code,
73
+ error_message: entry.error_message,
74
+ event: events[index]
75
+ })
76
+ elsif entry.event_id.present?
77
+ metrics.increment('tpt.event_bridge.publish_success')
78
+ end
79
+ end
80
+
81
+ res.data.entries
82
+ end
83
+
84
+ def make_client(options)
85
+ all_options = {
86
+ access_key_id: ENV.fetch('AWS_ACCESS_KEY_ID', nil),
87
+ secret_access_key: ENV.fetch('AWS_SECRET_ACCESS_KEY', nil),
88
+ region: ENV.fetch('AWS_REGION', 'us-east-1'),
89
+ http_open_timeout: 1, # default is 15
90
+ http_read_timeout: 1, # default is 60
91
+ }.merge(options)
92
+
93
+ Aws::EventBridge::Client.new(all_options)
94
+ end
95
+
96
+ def deep_camelize(hash)
97
+ hash.deep_transform_keys do |k|
98
+ # ignore keys like "__internal" (for now)
99
+ if k.to_s.starts_with?("_")
100
+ k
101
+ else
102
+ k.to_s.camelize(:lower)
103
+ end
104
+ end
105
+ end
106
+
107
+ def default_event_bus_name
108
+ ENV.fetch('TPT_SHARED_EVENT_BUS_NAME', 'shared-event-bus-dev')
109
+ end
110
+
111
+ def default_event_source
112
+ ENV.fetch('TPT_EVENT_SOURCE')
113
+ end
114
+
115
+ def logger
116
+ Rails.logger
117
+ end
118
+
119
+ def self.set_metrics(statsd)
120
+ unless statsd.is_a?(Datadog::Statsd)
121
+ raise "argument passed to EventBridgePublisher::set_metrics is not a Datadog::Statsd class instance"
122
+ end
123
+
124
+ @@statsd = statsd
125
+ end
126
+
127
+ def metrics
128
+ raise "metrics object not set; use EventBridgePublisher#set_metrics" unless @@statsd
129
+
130
+ @@statsd
131
+ end
132
+ end
133
+ end
@@ -5,6 +5,6 @@ class Tpt::Rails::ConfigurationGenerator < ::Rails::Generators::Base
5
5
 
6
6
  def configure
7
7
  route "mount Tpt::Rails::Engine => '/'"
8
- template 'config/initializers/tpt_rails.rb'
8
+ directory 'config'
9
9
  end
10
10
  end
@@ -0,0 +1,52 @@
1
+ # Puma can serve each request in a thread from an internal thread pool.
2
+ # The `threads` method setting takes two numbers: a minimum and maximum.
3
+ # Any libraries that use thread pools should be configured to match
4
+ # the maximum value specified for Puma. Default is set to 5 threads for minimum
5
+ # and maximum; this matches the default thread size of Active Record.
6
+ #
7
+ max_threads_count = ENV.fetch("RAILS_MAX_THREADS") { 5 }
8
+ min_threads_count = ENV.fetch("RAILS_MIN_THREADS") { max_threads_count }
9
+ threads min_threads_count, max_threads_count
10
+
11
+ # Specifies the `worker_timeout` threshold that Puma will use to wait before
12
+ # terminating a worker in development environments.
13
+ #
14
+ worker_timeout 3600 if ENV.fetch("RAILS_ENV", "development") == "development"
15
+
16
+ # Specifies the `port` that Puma will listen on to receive requests; default is 3000.
17
+ #
18
+ port ENV.fetch("PORT") { 3000 }
19
+
20
+ # Specifies the `environment` that Puma will run in.
21
+ #
22
+ environment ENV.fetch("RAILS_ENV") { "development" }
23
+
24
+ # Specifies the `pidfile` that Puma will use.
25
+ pidfile ENV.fetch("PIDFILE") { "tmp/pids/server.pid" }
26
+
27
+ before_fork do
28
+ Tpt::Rails::PumaStatsCollector.run(metrics_client: Tpt::Rails.statsd)
29
+ end
30
+
31
+ # Specifies the number of `workers` to boot in clustered mode.
32
+ # Workers are forked web server processes. If using threads and workers together
33
+ # the concurrency of the application would be max `threads` * `workers`.
34
+ # Workers do not work on JRuby or Windows (both of which do not support
35
+ # processes).
36
+
37
+ workers ENV.fetch("WEB_CONCURRENCY") { 2 }
38
+
39
+ on_worker_boot do
40
+ # See: https://devcenter.heroku.com/articles/deploying-rails-applications-with-the-puma-web-server#on-worker-boot
41
+ ActiveRecord::Base.establish_connection
42
+ end
43
+
44
+ # Use the `preload_app!` method when specifying a `workers` number.
45
+ # This directive tells Puma to first boot the application and load code
46
+ # before forking the application. This takes advantage of Copy On Write
47
+ # process behavior so workers use less memory.
48
+
49
+ preload_app!
50
+
51
+ # Allow puma to be restarted by `rails restart` command.
52
+ plugin :tmp_restart
@@ -36,6 +36,7 @@ metadata:
36
36
  - request_label:
37
37
  - frontend
38
38
  {{- if eq .Values.service.type "LoadBalancer" }}
39
+ external-dns.alpha.kubernetes.io/hostname: {{ .Values.hostname }}
39
40
  service.beta.kubernetes.io/aws-load-balancer-backend-protocol: "http"
40
41
  service.beta.kubernetes.io/aws-load-balancer-cross-zone-load-balancing-enabled: "true"
41
42
  service.beta.kubernetes.io/aws-load-balancer-internal: "0.0.0.0/0"
@@ -1,6 +1,8 @@
1
1
  name: <%= Tpt::Rails.app_name %>
2
2
  deploy_env: production
3
3
 
4
+ hostname: <%= Tpt::Rails.app_name %>-production.teacherspayteachers.com
5
+
4
6
  annotations:
5
7
  costEnv: production
6
8
 
@@ -1,6 +1,8 @@
1
1
  name: <%= Tpt::Rails.app_name %>
2
2
  deploy_env: staging
3
3
 
4
+ hostname: <%= Tpt::Rails.app_name %>-staging.teacherspayteachers.com
5
+
4
6
  annotations:
5
7
  costEnv: staging
6
8
 
@@ -1,6 +1,6 @@
1
1
  module Tpt
2
2
  module Rails
3
3
  # Do not change this manually. Our tooling will do it automatically.
4
- VERSION = '1.5.1'
4
+ VERSION = '1.6.1'
5
5
  end
6
6
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: tpt-rails
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.5.1
4
+ version: 1.6.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - TpT
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2021-05-19 00:00:00.000000000 Z
11
+ date: 2021-12-20 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -58,6 +58,34 @@ dependencies:
58
58
  - - "~>"
59
59
  - !ruby/object:Gem::Version
60
60
  version: '0.40'
61
+ - !ruby/object:Gem::Dependency
62
+ name: aws-sdk-core
63
+ requirement: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - "~>"
66
+ - !ruby/object:Gem::Version
67
+ version: 3.114.0
68
+ type: :runtime
69
+ prerelease: false
70
+ version_requirements: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - "~>"
73
+ - !ruby/object:Gem::Version
74
+ version: 3.114.0
75
+ - !ruby/object:Gem::Dependency
76
+ name: aws-sdk-eventbridge
77
+ requirement: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - "~>"
80
+ - !ruby/object:Gem::Version
81
+ version: 1.3.0
82
+ type: :runtime
83
+ prerelease: false
84
+ version_requirements: !ruby/object:Gem::Requirement
85
+ requirements:
86
+ - - "~>"
87
+ - !ruby/object:Gem::Version
88
+ version: 1.3.0
61
89
  - !ruby/object:Gem::Dependency
62
90
  name: sqlite3
63
91
  requirement: !ruby/object:Gem::Requirement
@@ -88,11 +116,14 @@ files:
88
116
  - app/helpers/tpt/rails/application_helper.rb
89
117
  - app/jobs/tpt/rails/application_job.rb
90
118
  - app/mailers/tpt/rails/application_mailer.rb
119
+ - app/models/tpt/rails/application_event.rb
91
120
  - app/models/tpt/rails/application_record.rb
121
+ - app/models/tpt/rails/event_bridge_publisher.rb
92
122
  - app/views/layouts/tpt/rails/application.html.erb
93
123
  - config/routes.rb
94
124
  - lib/generators/tpt/rails/configuration/configuration_generator.rb
95
125
  - lib/generators/tpt/rails/configuration/templates/config/initializers/tpt_rails.rb
126
+ - lib/generators/tpt/rails/configuration/templates/config/puma.rb
96
127
  - lib/generators/tpt/rails/continuous_integration/continuous_integration_generator.rb
97
128
  - lib/generators/tpt/rails/continuous_integration/templates/ci/Dockerfile.tt
98
129
  - lib/generators/tpt/rails/continuous_integration/templates/ci/docker-compose.yaml.tt