google_cloud_run 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 1978e88dc8a5bfb84eab657f11332750562931fc15d4ed9594e22ab0278b76f1
4
+ data.tar.gz: 45d40f141a603729fac0d75c54cfa4ebdde0c986500c357897db89a010ff18d1
5
+ SHA512:
6
+ metadata.gz: 6eebceb1d26a294f9f990464e2ca611dabffa7a1a02fd8605d0756d5c4e5f884d988805f5e7cc58064af9b12aad52f0de05d63c1866f9b5bddeac6eb3db46524
7
+ data.tar.gz: e39fa2604dbc229083c43285ba066712a70df61733ccd21aec5ce62758a2520d486f5ebef8b99e36bca78e2c9f56c81351f939d99a85c7f2b132b07ffc8d499b
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright 2021 Matthias Kadenbach
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,200 @@
1
+ # Rails on Google Cloud Run
2
+
3
+ * Logging
4
+ * Error Reporting
5
+ * Active Job via [Cloud Tasks](https://cloud.google.com/tasks), including delayed jobs.
6
+ * Minor patches for better compatibility
7
+ * Works with Ruby 3 and Rails 6
8
+
9
+
10
+ ## Usage
11
+
12
+ ```ruby
13
+ logger.info "Hello World"
14
+
15
+ logger.info do
16
+ "Expensive logging operation, only run when logged"
17
+ end
18
+
19
+ logger.info "Labels work, too!", my_label: "works", another_one: "great"
20
+ ```
21
+
22
+ All Google Cloud Logging Severities are supported:
23
+
24
+ ```
25
+ logger.default (or logger.unknown) - The log entry has no assigned severity level.
26
+ logger.debug - Debug or trace information.
27
+ logger.info - Routine information, such as ongoing status or performance.
28
+ logger.notice - Normal but significant events, such as start up, shut down, or a configuration change.
29
+ logger.warning (or logger.warn) - Warning events might cause problems.
30
+ logger.error - Error events are likely to cause problems.
31
+ logger.critical (or logger.fatal) - Critical events cause more severe problems or outages.
32
+ logger.alert - A person must take an action immediately.
33
+ logger.emergency - One or more systems are unusable.
34
+ ```
35
+
36
+
37
+ ## Installation
38
+
39
+ Add the gem to your Gemfile and run `bundle install`.
40
+
41
+ ```ruby
42
+ # Gemfile
43
+ group :production do
44
+ gem 'google_cloud_run'
45
+ end
46
+ ```
47
+
48
+
49
+ In your production config:
50
+
51
+ ```ruby
52
+ # config/environments/production.rb
53
+
54
+ config.log_level = :g_notice
55
+ config.logger = GoogleCloudRun::Logger.new
56
+
57
+ config.active_job.queue_adapter = :google_cloudrun_tasks
58
+ config.google_cloudrun.job_queue_default_region = "us-central1"
59
+ config.google_cloudrun.job_callback_url = "https://your-domain.com/rails/google_cloudrun/job_callback"
60
+ ```
61
+
62
+ Set the default queue:
63
+
64
+ ```ruby
65
+ # app/jobs/application_job.rb
66
+ queue_as "my-queue"
67
+
68
+ # or if `config.google_cloudrun.job_queue_default_region` isn't set:
69
+ queue_as "us-central1/my-queue"
70
+ ```
71
+
72
+ ---
73
+
74
+ In the default production config, the logger is wrapped around
75
+ a `ENV["RAILS_LOG_TO_STDOUT"].present?` block. I usually just
76
+ remove this block so I don't have to actually set this ENV var.
77
+
78
+ You can also remove `config.log_formatter` as we don't need it anymore.
79
+
80
+ I recommend logging `:g_notice` and higher. Rails logs a lot of noise when logging
81
+ `:info` and higher.
82
+
83
+
84
+ ## Configuration
85
+
86
+ You can change more settings in `config/environments/production.rb`. See below
87
+ for the default configuration.
88
+
89
+ ```ruby
90
+ # Enable Google Cloud Logging
91
+ config.google_cloudrun.logger = true
92
+
93
+ # Set output (STDERR or STDOUT)
94
+ config.google_cloudrun.out = STDERR
95
+
96
+ # Add source location (file, line number, method) to each log
97
+ config.google_cloudrun.logger_source_location = true
98
+
99
+ # Run Proc to assign current user as label to each log
100
+ config.google_cloudrun.logger_user = nil
101
+
102
+
103
+ # Enable Error Reporting
104
+ config.google_cloudrun.error_reporting = true
105
+
106
+ # Assign a default severity level to exceptions
107
+ config.google_cloudrun.error_reporting_exception_severity = :critical
108
+
109
+ # Run Proc to assign current user to Error Report
110
+ config.google_cloudrun.error_reporting_user = nil
111
+
112
+ # Turn logs into error reports for this severity and higher.
113
+ # Set to nil to disable.
114
+ config.google_cloudrun.error_reporting_level = :error
115
+
116
+ # When log is turned into error report, discard the original
117
+ # log and only report the error.
118
+ # Set to false to log and report the error at the same time.
119
+ config.google_cloudrun.error_reporting_discard_log = true
120
+
121
+
122
+ # Don't log or error report the following exceptions,
123
+ # because Cloud Run will create access logs for us already.
124
+ config.google_cloudrun.silence_exceptions = [
125
+ ActionController::RoutingError,
126
+ ActionController::MethodNotAllowed,
127
+ ActionController::UnknownHttpMethod,
128
+ ActionController::NotImplemented,
129
+ ActionController::UnknownFormat,
130
+ ActionController::BadRequest,
131
+ ActionController::ParameterMissing,
132
+ ]
133
+
134
+
135
+ # Set Rails' request id to the trace id from X-Cloud-Trace-Context header
136
+ # as set by Cloud Run.
137
+ config.google_cloudrun.patch_request_id = true
138
+
139
+
140
+ # Enable Jobs via Cloud Tasks
141
+ config.google_cloudrun.jobs = true
142
+
143
+ # Set the default Google Cloud Task region, i.e. us-central1
144
+ config.google_cloudrun.job_queue_default_region = nil
145
+
146
+ # Google Cloud Tasks will call this url to execute the job
147
+ config.google_cloudrun.job_callback_url = nil # required, see above
148
+
149
+ # The default route for the callback url.
150
+ config.google_cloudrun.job_callback_path = "/rails/google_cloudrun/job_callback"
151
+
152
+ # Time for a job to run in seconds, default is 30min.
153
+ # Use `timeout_after 5.minutes` to configure a job individually.
154
+ config.google_cloudrun.job_timeout_sec = 1800 # (min 15s, max 30m)
155
+ ```
156
+
157
+ ---
158
+
159
+ Both `error_reporting_user` and `logger_user` expect a Proc like this:
160
+
161
+ ```ruby
162
+ config.google_cloudrun.logger_user = Proc.new do |request|
163
+ # extract and return user id from request, example:
164
+ request.try { cookie_jar.encrypted[:user_id] }
165
+ end
166
+ ```
167
+
168
+ ---
169
+
170
+ An example job:
171
+
172
+ ```ruby
173
+ class MyJob < ApplicationJob
174
+ queue_as "us-central1/urgent"
175
+ timeout_after 1.minute # min 15s, max 30m, overrides config.google_cloudrun.job_timeout_sec
176
+
177
+ def perform(*args)
178
+ # Do something
179
+ end
180
+ end
181
+ ```
182
+
183
+ ## Cloud Task considerations
184
+
185
+ * Cloud Tasks are a better fit than Google Pub/Sub.
186
+ [Read more](https://cloud.google.com/pubsub/docs/choosing-pubsub-or-cloud-tasks#detailed-feature-comparison)
187
+ * I'd recommend to create two different Cloud Run services.
188
+ One for HTTP requests (aka Heroku Dynos) and another service
189
+ for jobs (aka Heroku Workers). Set the `Request Timeout` for
190
+ the request-bound service to something like `15s`, and for workers
191
+ to `1800s` or match `config.google_cloudrun.job_timeout_sec`.
192
+ * Cloud Task execution calls are authenticated with a Google-issued
193
+ OIDC token. So even though `/rails/google_cloudrun/job_callback` is publicly
194
+ available, without a valid token, no job will be executed.
195
+ * Cloud Task job processing is async. It supports multiple queues. Delayed jobs
196
+ are natively supported through Cloud Task. Priority jobs are not supported, use
197
+ different queues for that, i.e. "urgent", or "low-priority". Timeouts can be set
198
+ globally or per job-basis (min 15s, max 30m).
199
+ Retries are natively supported by Cloud Tasks.
200
+
@@ -0,0 +1,32 @@
1
+ module GoogleCloudRun
2
+ class JobsController < ActionController::Base
3
+ skip_before_action :verify_authenticity_token
4
+
5
+ def callback
6
+ # verify User-Agent and Content-Type
7
+ return head :bad_request unless request.user_agent == "Google-Cloud-Tasks"
8
+ return head :bad_request unless request.headers["Content-type"].include?("application/json")
9
+ return head :bad_request unless request.headers["Authorization"].start_with?("Bearer")
10
+
11
+ # verify Bearer token
12
+ begin
13
+ r = Google::Auth::IDTokens.verify_oidc request.headers["Authorization"]&.delete_prefix("Bearer")&.strip
14
+ rescue => e
15
+ Rails.logger.warning "Google Cloud Run Job callback failed: #{e.message}"
16
+ return head :bad_request
17
+ end
18
+
19
+ # parse JSON body
20
+ begin
21
+ body = JSON.parse(request.body.read)
22
+ rescue => e
23
+ raise "Google Cloud Run Job callback failed: Unable to parse JSON body: #{e.message}"
24
+ end
25
+
26
+ # execute the job
27
+ ActiveJob::Base.execute body
28
+
29
+ head :ok
30
+ end
31
+ end
32
+ end
data/config/routes.rb ADDED
@@ -0,0 +1,3 @@
1
+ Rails.application.routes.draw do
2
+ post Rails.application.config.google_cloudrun.job_callback_path => "google_cloud_run/jobs#callback", as: :rails_google_cloudrun_job_callback
3
+ end
@@ -0,0 +1,5 @@
1
+ module GoogleCloudRun
2
+ class Engine < ::Rails::Engine
3
+ isolate_namespace GoogleCloudRun
4
+ end
5
+ end
@@ -0,0 +1,153 @@
1
+ module GoogleCloudRun
2
+ class LogEntry
3
+ include ::Logger::Severity
4
+
5
+ attr_accessor :severity,
6
+ :message,
7
+ :labels,
8
+ :timestamp,
9
+ :request,
10
+ :user,
11
+ :location_path, :location_line, :location_method,
12
+ :project_id
13
+
14
+ def initialize
15
+ @severity = G_DEFAULT
16
+ @timestamp = Time.now.utc
17
+ @insert_id = SecureRandom.uuid
18
+ end
19
+
20
+ def to_json
21
+ raise "labels must be hash" if !@labels.blank? && !@labels.is_a?(Hash)
22
+
23
+ labels["user"] = @user unless @user.blank?
24
+
25
+ j = {}
26
+
27
+ j["logging.googleapis.com/insertId"] = @insert_id
28
+ j["severity"] = Severity.to_s(Severity.mapping(@severity))
29
+ j["message"] = @message.is_a?(String) ? @message.strip : @message.inspect
30
+ j["timestampSeconds"] = @timestamp.to_i
31
+ j["timestampNanos"] = @timestamp.nsec
32
+ j["logging.googleapis.com/labels"] = @labels unless @labels.blank?
33
+
34
+ if @request
35
+ # https://cloud.google.com/logging/docs/reference/v2/rest/v2/LogEntry#httprequest
36
+ j["httpRequest"] = {}
37
+ j["httpRequest"]["requestMethod"] = @request&.method.to_s
38
+ j["httpRequest"]["requestUrl"] = @request&.url.to_s
39
+ j["httpRequest"]["userAgent"] = @request&.headers["user-agent"].to_s unless @request&.headers["user-agent"].blank?
40
+ j["httpRequest"]["remoteIp"] = @request&.remote_ip.to_s
41
+ j["httpRequest"]["referer"] = @request&.headers["referer"].to_s unless @request&.headers["referer"].blank?
42
+
43
+ trace, span, sample = GoogleCloudRun.parse_trace_context(@request&.headers["X-Cloud-Trace-Context"])
44
+ j["logging.googleapis.com/trace"] = "projects/#{@project_id}/traces/#{trace}" unless trace.blank?
45
+ j["logging.googleapis.com/spanId"] = span unless span.blank?
46
+ j["logging.googleapis.com/trace_sampled"] = sample unless sample.nil?
47
+ end
48
+
49
+ if @location_path || @location_line || @location_method
50
+ j["logging.googleapis.com/sourceLocation"] = {}
51
+ j["logging.googleapis.com/sourceLocation"]["function"] = @location_method.to_s
52
+ j["logging.googleapis.com/sourceLocation"]["file"] = @location_path.to_s
53
+ j["logging.googleapis.com/sourceLocation"]["line"] = @location_line.to_i
54
+ end
55
+
56
+ j.to_json
57
+ end
58
+ end
59
+
60
+ # https://cloud.google.com/error-reporting/reference/rest/v1beta1/projects.events/report#ReportedErrorEvent
61
+ # https://cloud.google.com/error-reporting/docs/formatting-error-messages
62
+ class ErrorReportingEntry
63
+ include ::Logger::Severity
64
+
65
+ attr_accessor :severity,
66
+ :exception,
67
+ :project_id,
68
+ :message,
69
+ :labels,
70
+ :timestamp,
71
+ :request,
72
+ :user,
73
+ :location_path, :location_line, :location_method,
74
+ :context_service, :context_version
75
+
76
+ def initialize
77
+ @severity = G_CRITICAL
78
+ @timestamp = Time.now.utc
79
+ @insert_id = SecureRandom.uuid
80
+ end
81
+
82
+ def to_json
83
+ raise "labels must be hash" if !@labels.blank? && !@labels.is_a?(Hash)
84
+
85
+ j = {}
86
+
87
+ j["@type"] = "type.googleapis.com/google.devtools.clouderrorreporting.v1beta1.ReportedErrorEvent"
88
+ j["logging.googleapis.com/insertId"] = @insert_id
89
+ j["severity"] = Severity.to_s(Severity.mapping(@severity))
90
+ j["eventTime"] = @timestamp.strftime("%FT%T.%9NZ")
91
+ j["logging.googleapis.com/labels"] = @labels unless @labels.blank?
92
+
93
+ if @context_service || @context_version
94
+ j["serviceContext"] = {}
95
+ j["serviceContext"]["service"] = @context_service.to_s
96
+ j["serviceContext"]["version"] = @context_version.to_s
97
+ end
98
+
99
+ if @request
100
+ # https://cloud.google.com/logging/docs/reference/v2/rest/v2/LogEntry#httprequest
101
+ j["httpRequest"] = {}
102
+ j["httpRequest"]["requestMethod"] = @request&.method.to_s
103
+ j["httpRequest"]["requestUrl"] = @request&.url.to_s
104
+ j["httpRequest"]["userAgent"] = @request&.headers["user-agent"].to_s unless @request&.headers["user-agent"].blank?
105
+ j["httpRequest"]["remoteIp"] = @request&.remote_ip.to_s
106
+ j["httpRequest"]["referer"] = @request&.headers["referer"].to_s unless @request&.headers["referer"].blank?
107
+
108
+ trace, span, sample = GoogleCloudRun.parse_trace_context(@request&.headers["X-Cloud-Trace-Context"])
109
+ j["logging.googleapis.com/trace"] = "projects/#{@project_id}/traces/#{trace}" unless trace.blank?
110
+ j["logging.googleapis.com/spanId"] = span unless span.blank?
111
+ j["logging.googleapis.com/trace_sampled"] = sample unless sample.nil?
112
+ end
113
+
114
+ if @exception
115
+ j["message"] = @exception.class.to_s
116
+
117
+ e_message = @exception&.message.to_s.strip
118
+ unless e_message.blank?
119
+ j["message"] << ": " + e_message + "\n"
120
+ end
121
+
122
+ j["message"] << @exception&.backtrace.join("\n")
123
+ else
124
+ j["message"] = @message.is_a?(String) ? @message.strip : @message.inspect
125
+ end
126
+
127
+ j["context"] = {}
128
+
129
+ if @request
130
+ # https://cloud.google.com/logging/docs/reference/v2/rest/v2/LogEntry#httprequest
131
+ j["context"]["httpRequest"] = {}
132
+ j["context"]["httpRequest"]["method"] = @request&.method
133
+ j["context"]["httpRequest"]["url"] = @request&.url
134
+ j["context"]["httpRequest"]["userAgent"] = @request&.headers["user-agent"] unless @request&.headers["user-agent"].blank?
135
+ j["context"]["httpRequest"]["remoteIp"] = @request&.remote_ip
136
+ j["context"]["httpRequest"]["referrer"] = @request&.headers["referer"] unless @request&.headers["referer"].blank?
137
+ end
138
+
139
+ if @user
140
+ j["context"]["user"] = @user
141
+ end
142
+
143
+ if @location_path || @location_line || @location_method
144
+ j["context"]["reportLocation"] = {}
145
+ j["context"]["reportLocation"]["filePath"] = @location_path.to_s
146
+ j["context"]["reportLocation"]["lineNumber"] = @location_line.to_i
147
+ j["context"]["reportLocation"]["functionName"] = @location_method.to_s
148
+ end
149
+
150
+ j.to_json
151
+ end
152
+ end
153
+ end
@@ -0,0 +1,32 @@
1
+ module GoogleCloudRun
2
+ def self.exception_interceptor(request, exception)
3
+
4
+ # ref: https://cloud.google.com/error-reporting/reference/rest/v1beta1/projects.events/report#reportederrorevent
5
+
6
+ return false if Rails.application.config.google_cloudrun.silence_exceptions.any? { |e| exception.is_a?(e) }
7
+
8
+ l = ErrorReportingEntry.new
9
+ l.project_id = GoogleCloudRun.project_id
10
+ l.severity = Rails.application.config.google_cloudrun.error_reporting_exception_severity
11
+ l.exception = exception
12
+ l.request = request
13
+
14
+ l.context_service = GoogleCloudRun.k_service
15
+ l.context_version = GoogleCloudRun.k_revision
16
+
17
+ # attach user to entry
18
+ p = Rails.application.config.google_cloudrun.error_reporting_user
19
+ if p && p.is_a?(Proc)
20
+ begin
21
+ l.user = p.call(request)
22
+ rescue
23
+ # TODO ignore or log?
24
+ end
25
+ end
26
+
27
+ Rails.application.config.google_cloudrun.out.puts l.to_json
28
+ Rails.application.config.google_cloudrun.out.flush
29
+
30
+ return true
31
+ end
32
+ end
@@ -0,0 +1,135 @@
1
+ module ActiveJob
2
+ module QueueAdapters
3
+ class GoogleCloudrunTasksAdapter
4
+ def initialize
5
+ @client = Google::Cloud::Tasks.cloud_tasks
6
+ @project_id = GoogleCloudRun.project_id
7
+ @service_account_email = GoogleCloudRun.default_service_account_email
8
+ @default_job_timeout_sec = Rails.application.config.google_cloudrun.job_timeout_sec
9
+ @job_callback_url = Rails.application.config.google_cloudrun.job_callback_url
10
+ @queue_default_region = Rails.application.config.google_cloudrun.job_queue_default_region
11
+
12
+ if @job_callback_url.blank? || !@job_callback_url.end_with?(Rails.application.config.google_cloudrun.job_callback_path)
13
+ raise "Set config.google_cloudrun.job_callback_url to 'https://your-domain.com#{Rails.application.config.google_cloudrun.job_callback_path}'"
14
+ end
15
+
16
+ if !@job_callback_url.start_with?("https://")
17
+ raise "config.google_cloudrun.job_callback_url must start with https://"
18
+ end
19
+ end
20
+
21
+ def enqueue(job)
22
+ create_cloudtask(job.class,
23
+ job.job_id,
24
+ job.queue_name,
25
+ local_timeout(job) || @default_job_timeout_sec,
26
+ nil,
27
+ job.serialize)
28
+ end
29
+
30
+ def enqueue_at(job, timestamp)
31
+ create_cloudtask(job.class,
32
+ job.job_id,
33
+ job.queue_name,
34
+ local_timeout(job) || @default_job_timeout_sec,
35
+ timestamp,
36
+ job.serialize)
37
+ end
38
+
39
+ private
40
+
41
+ def create_cloudtask(job_name, job_id, full_queue_name, job_timeout, scheduled_at, job)
42
+ return if !Rails.application.config.google_cloudrun.jobs
43
+
44
+ region, queue_name = parse_full_queue_name(full_queue_name)
45
+ queue = @client.queue_path project: @project_id, location: region, queue: queue_name
46
+
47
+ task = build_task_request(
48
+ "projects/#{@project_id}/locations/#{region}/queues/#{queue_name}/tasks/#{job_id}",
49
+ @job_callback_url,
50
+ @service_account_email,
51
+ job.to_json,
52
+ job_timeout,
53
+ scheduled_at,
54
+ )
55
+
56
+ response = nil
57
+ begin
58
+ response = @client.create_task parent: queue, task: task
59
+ rescue => e
60
+ raise "Failed sending job #{job_name}(#{job_id}) to queue '#{region}/#{queue_name}'. #{e.message}"
61
+ end
62
+ if response.nil?
63
+ raise "Failed sending job #{job_name}(#{job_id}) to queue '#{region}/#{queue_name}'. Google didn't return a response."
64
+ end
65
+
66
+ Rails.logger&.notice "Job #{job_name}(#{job_id}) sent to queue '#{region}/#{queue_name}'"
67
+ end
68
+
69
+ def build_task_request(name, url, service_account_email, body, job_timeout, scheduled_at)
70
+ # ref: https://cloud.google.com/tasks/docs/reference/rest/v2/projects.locations.queues.tasks#Task
71
+ req = {
72
+ name: name,
73
+ http_request: {
74
+ oidc_token: { service_account_email: service_account_email },
75
+ headers: { "Content-Type": "application/json" },
76
+ http_method: "POST",
77
+ url: url,
78
+ body: body,
79
+ },
80
+ }
81
+
82
+ d = Google::Protobuf::Duration.new
83
+ d.seconds = job_timeout.to_i
84
+ req[:dispatch_deadline] = d
85
+
86
+ if scheduled_at
87
+ t = Google::Protobuf::Timestamp.new
88
+ t.seconds = Time.at(scheduled_at).utc.to_i
89
+ req[:schedule_time] = t
90
+ end
91
+
92
+ return req
93
+ end
94
+
95
+ def parse_full_queue_name(queue_name)
96
+ # config.active_job.queue_name_prefix will add an underscore,
97
+ # queue names can't have underscores. Let's turn it into a hyphen.
98
+ queue_name = queue_name.gsub("_", "-")
99
+
100
+ # see if we have something like this: region/queue
101
+ parts = queue_name.split("/")
102
+ if parts.size == 2
103
+ return parts[0], parts[1]
104
+ end
105
+
106
+ if @queue_default_region.blank?
107
+ raise "queue_as \"#{queue_name}\" needs region: \"region/#{queue_name}\" or set config.google_cloudrun.job_queue_default_region"
108
+ end
109
+
110
+ # use our default region
111
+ return @queue_default_region, queue_name
112
+ end
113
+
114
+ def local_timeout(job)
115
+ begin
116
+ job.class.class_variable_get(:@@google_cloudrun_job_timeout)
117
+ rescue
118
+ nil
119
+ end
120
+ end
121
+ end
122
+ end
123
+ end
124
+
125
+ module GoogleCloudRun
126
+ module TimeoutAfterExtension
127
+ extend ActiveSupport::Concern
128
+
129
+ class_methods do
130
+ def timeout_after(t)
131
+ self.class_variable_set(:@@google_cloudrun_job_timeout, t)
132
+ end
133
+ end
134
+ end
135
+ end
@@ -0,0 +1,316 @@
1
+ module GoogleCloudRun
2
+ class Logger
3
+ include ::Logger::Severity
4
+
5
+ def initialize
6
+ @level = G_INFO
7
+ @formatter = DummyFormatter.new
8
+ @out = Rails.application.config.google_cloudrun.out
9
+ @project_id = GoogleCloudRun.project_id
10
+ end
11
+
12
+ def level=(level)
13
+ @level = Severity.mapping(level)
14
+ end
15
+
16
+ def level?(level)
17
+ Severity.mapping(level) >= @level
18
+ end
19
+
20
+ def log(severity, msg = nil, progname = nil, **labels, &block)
21
+ labels["progname"] = progname unless progname.blank?
22
+ write(severity, msg, labels, &block)
23
+ end
24
+
25
+ def default(msg = nil, **labels, &block)
26
+ write(G_DEFAULT, msg, labels, &block)
27
+ end
28
+
29
+ def default?
30
+ self.level?(G_DEFAULT)
31
+ end
32
+
33
+ def default!
34
+ self.level = G_DEFAULT
35
+ end
36
+
37
+ def debug(msg = nil, **labels, &block)
38
+ write(G_DEBUG, msg, labels, &block)
39
+ end
40
+
41
+ def debug?
42
+ self.level?(G_DEBUG)
43
+ end
44
+
45
+ def debug!
46
+ self.level = G_DEBUG
47
+ end
48
+
49
+ def info(msg = nil, **labels, &block)
50
+ write(G_INFO, msg, labels, &block)
51
+ end
52
+
53
+ def info?
54
+ self.level?(G_INFO)
55
+ end
56
+
57
+ def info!
58
+ self.level = G_INFO
59
+ end
60
+
61
+ def notice(msg = nil, **labels, &block)
62
+ write(G_NOTICE, msg, labels, &block)
63
+ end
64
+
65
+ def notice?
66
+ self.level?(G_NOTICE)
67
+ end
68
+
69
+ def notice!
70
+ self.level = G_NOTICE
71
+ end
72
+
73
+ def warning(msg = nil, **labels, &block)
74
+ write(G_WARNING, msg, labels, &block)
75
+ end
76
+
77
+ def warning?
78
+ self.level?(G_WARNING)
79
+ end
80
+
81
+ def warning!
82
+ self.level = G_WARNING
83
+ end
84
+
85
+ def error(msg = nil, **labels, &block)
86
+ write(G_ERROR, msg, labels, &block)
87
+ end
88
+
89
+ def error?
90
+ self.level?(G_ERROR)
91
+ end
92
+
93
+ def error!
94
+ self.level = G_ERROR
95
+ end
96
+
97
+ def critical(msg = nil, **labels, &block)
98
+ write(G_CRITICAL, msg, labels, &block)
99
+ end
100
+
101
+ def critical?
102
+ self.level?(G_CRITICAL)
103
+ end
104
+
105
+ def critical!
106
+ self.level = G_CRITICAL
107
+ end
108
+
109
+ def alert(msg = nil, **labels, &block)
110
+ write(G_ALERT, msg, labels, &block)
111
+ end
112
+
113
+ def alert?
114
+ self.level?(G_ALERT)
115
+ end
116
+
117
+ def alert!
118
+ self.level = G_ALERT
119
+ end
120
+
121
+ def emergency(msg = nil, **labels, &block)
122
+ write(G_EMERGENCY, msg, labels, &block)
123
+ end
124
+
125
+ def emergency?
126
+ self.level?(G_EMERGENCY)
127
+ end
128
+
129
+ def emergency!
130
+ self.level = G_EMERGENCY
131
+ end
132
+
133
+ def <<(msg)
134
+ log(G_DEBUG, msg)
135
+ end
136
+
137
+ # called by LoggerMiddleware
138
+ def inject_request(request)
139
+ Thread.current[thread_key] = request
140
+ end
141
+
142
+ # called by ActiveSupport::LogSubscriber.flush_all!
143
+ def flush
144
+ Thread.current[thread_key] = nil
145
+ @out.flush
146
+ end
147
+
148
+ def datetime_format
149
+ "%FT%T.%9NZ" # RFC3339 UTC "Zulu" format, with nanosecond resolution and up to nine fractional digits
150
+ end
151
+
152
+ def formatter
153
+ @formatter
154
+ end
155
+
156
+ # implement ::Logger interface, but do nothing
157
+ def close; end
158
+
159
+ def reopen(logdev = nil); end
160
+
161
+ def datetime_format=(format); end
162
+
163
+ def formatter=(formatter); end
164
+
165
+ alias_method :warn, :warning
166
+ alias_method :warn!, :warning!
167
+ alias_method :warn?, :warning?
168
+ alias_method :unknown, :default
169
+ alias_method :fatal, :critical
170
+ alias_method :fatal!, :critical!
171
+ alias_method :fatal?, :critical?
172
+ alias_method :add, :log
173
+ alias_method :sev_threshold, :level=
174
+
175
+ private
176
+
177
+ def should_log?(severity)
178
+ Rails.application.config.google_cloudrun.logger && self.level?(severity)
179
+ end
180
+
181
+ def should_error_report?(severity)
182
+ Rails.application.config.google_cloudrun.error_reporting &&
183
+ !Rails.application.config.google_cloudrun.error_reporting_level.nil? &&
184
+ Severity.mapping(severity) >= Severity.mapping(Rails.application.config.google_cloudrun.error_reporting_level)
185
+ end
186
+
187
+ def write(severity, msg, labels = {}, &block)
188
+ should_log = should_log?(severity)
189
+ should_error_report = should_error_report?(severity)
190
+ return false if !should_log && !should_error_report
191
+
192
+ # execute given block
193
+ msg = block.call if block
194
+
195
+ # write error report
196
+ if should_error_report
197
+ write_error_report(severity, msg, labels)
198
+
199
+ # return early if we don't want to log as well
200
+ return true if Rails.application.config.google_cloudrun.error_reporting_discard_log
201
+ end
202
+
203
+ # write log
204
+ if should_log
205
+ write_log(severity, msg, labels)
206
+ end
207
+
208
+ return true
209
+ end
210
+
211
+ def write_log(severity, msg, labels)
212
+ l = GoogleCloudRun::LogEntry.new
213
+ l.severity = severity
214
+ l.message = msg
215
+ l.labels = labels
216
+ l.request = current_request
217
+ l.project_id = @project_id
218
+
219
+ # set caller location
220
+ if Rails.application.config.google_cloudrun.logger_source_location
221
+ loc = caller_locations(3, 1)&.first
222
+ if loc
223
+ l.location_path = loc.path
224
+ l.location_line = loc.lineno
225
+ l.location_method = loc.label
226
+ end
227
+ end
228
+
229
+ # attach user to entry
230
+ p = Rails.application.config.google_cloudrun.logger_user
231
+ if p && p.is_a?(Proc)
232
+ begin
233
+ l.user = p.call(current_request)
234
+ rescue
235
+ raise
236
+ # TODO ignore or log?
237
+ end
238
+ end
239
+
240
+ @out.puts l.to_json
241
+ end
242
+
243
+ def write_error_report(severity, msg, labels)
244
+ l = ErrorReportingEntry.new
245
+ l.severity = severity
246
+ l.request = current_request
247
+ l.labels = labels
248
+ l.message = msg
249
+ l.project_id = @project_id
250
+
251
+ # set caller location
252
+ loc = caller_locations(3, 1)&.first
253
+ if loc
254
+ l.location_path = loc.path
255
+ l.location_line = loc.lineno
256
+ l.location_method = loc.label
257
+ end
258
+
259
+ # set context
260
+ l.context_service = GoogleCloudRun.k_service
261
+ l.context_version = GoogleCloudRun.k_revision
262
+
263
+ # attach user to entry
264
+ p = Rails.application.config.google_cloudrun.error_reporting_user
265
+ if p && p.is_a?(Proc)
266
+ begin
267
+ l.user = p.call(current_request)
268
+ rescue
269
+ # TODO ignore or log?
270
+ end
271
+ end
272
+
273
+ @out.puts l.to_json
274
+ end
275
+
276
+ def current_request
277
+ Thread.current[thread_key]
278
+ end
279
+
280
+ def thread_key
281
+ # We use our object ID here to avoid conflicting with other instances
282
+ thread_key = @thread_key ||= "google_cloudrun_logging_request:#{object_id}"
283
+ end
284
+ end
285
+
286
+ class LoggerMiddleware
287
+ def initialize(app)
288
+ @app = app
289
+ end
290
+
291
+ # A middleware which injects the request into the Rails.logger
292
+ def call(env)
293
+ request = ActionDispatch::Request.new(env)
294
+ Rails.logger.inject_request(request)
295
+ @app.call(env)
296
+ ensure
297
+ ActiveSupport::LogSubscriber.flush_all!
298
+ end
299
+ end
300
+
301
+ class DummyFormatter < ::Logger::Formatter
302
+ def call(severity, timestamp, progname, msg)
303
+ # we bypass all formatters
304
+ end
305
+ end
306
+
307
+ module SilenceExceptions
308
+ private
309
+
310
+ def log_error(_request, wrapper)
311
+ exception = wrapper.exception
312
+ return if Rails.application.config.google_cloudrun.silence_exceptions.any? { |e| exception.is_a?(e) }
313
+ super
314
+ end
315
+ end
316
+ end
@@ -0,0 +1,60 @@
1
+ module GoogleCloudRun
2
+ class Railtie < ::Rails::Railtie
3
+ config.google_cloudrun = ActiveSupport::OrderedOptions.new
4
+
5
+ config.google_cloudrun.out = STDERR
6
+
7
+ config.google_cloudrun.logger = true
8
+ config.google_cloudrun.logger_source_location = true
9
+ config.google_cloudrun.logger_user = nil
10
+
11
+ config.google_cloudrun.error_reporting = true
12
+ config.google_cloudrun.error_reporting_exception_severity = :critical
13
+ config.google_cloudrun.error_reporting_user = nil
14
+ config.google_cloudrun.error_reporting_level = :error
15
+ config.google_cloudrun.error_reporting_discard_log = true
16
+
17
+ config.google_cloudrun.silence_exceptions = [
18
+ ActionController::RoutingError,
19
+ ActionController::MethodNotAllowed,
20
+ ActionController::UnknownHttpMethod,
21
+ ActionController::NotImplemented,
22
+ ActionController::UnknownFormat,
23
+ ActionController::BadRequest,
24
+ ActionController::ParameterMissing,
25
+ ]
26
+
27
+ config.google_cloudrun.patch_request_id = true
28
+
29
+ config.google_cloudrun.jobs = true
30
+ config.google_cloudrun.job_queue_default_region = nil
31
+ config.google_cloudrun.job_callback_url = nil # required
32
+ config.google_cloudrun.job_callback_path = "/rails/google_cloudrun/job_callback"
33
+ config.google_cloudrun.job_timeout_sec = 1800 # 30 min (min 15s, max 30m)
34
+
35
+ # ref: https://guides.rubyonrails.org/rails_on_rack.html#internal-middleware-stack
36
+
37
+ initializer "google_cloud_run" do |app|
38
+ if app.config.google_cloudrun.error_reporting
39
+ ActionDispatch::DebugExceptions.register_interceptor GoogleCloudRun.method(:exception_interceptor)
40
+ end
41
+
42
+ if app.config.google_cloudrun.logger
43
+ app.config.middleware.insert_after Rails::Rack::Logger, GoogleCloudRun::LoggerMiddleware
44
+ end
45
+
46
+ if app.config.google_cloudrun.patch_request_id
47
+ app.config.middleware.insert_before ActionDispatch::RequestId, GoogleCloudRun::RequestId
48
+ end
49
+
50
+ # https://stackoverflow.com/a/52475865/2142441
51
+ if config.google_cloudrun.silence_exceptions.size > 0
52
+ ActiveSupport.on_load(:action_controller) do
53
+ ActionDispatch::DebugExceptions.prepend GoogleCloudRun::SilenceExceptions
54
+ end
55
+ end
56
+
57
+ ActiveJob::Base.send(:include, GoogleCloudRun::TimeoutAfterExtension)
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,16 @@
1
+ module GoogleCloudRun
2
+ class RequestId
3
+ def initialize(app)
4
+ @app = app
5
+ end
6
+
7
+ # A middleware to replace X-Request-Id with X-Cloud-Trace-Context's Trace ID
8
+ # ref: https://github.com/rails/rails/blob/6-1-stable/actionpack/lib/action_dispatch/middleware/request_id.rb
9
+ # ref: https://github.com/Octo-Labs/heroku-request-id/blob/master/lib/heroku-request-id/railtie.rb
10
+ def call(env)
11
+ req = ActionDispatch::Request.new env
12
+ trace, _, _ = GoogleCloudRun.parse_trace_context(req.headers["X-Cloud-Trace-Context"])
13
+ @app.call(env).tap { |_status, headers, _body| headers["X-Request-Id"] = trace }
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,124 @@
1
+ class Logger
2
+ module Severity
3
+ # Ruby/Rails default severities:
4
+ # Low-level information, mostly for developers.
5
+ # DEBUG = 0
6
+ # Generic (useful) information about system operation.
7
+ # INFO = 1
8
+ # A warning.
9
+ # WARN = 2
10
+ # A handleable error condition.
11
+ # ERROR = 3
12
+ # An unhandleable error that results in a program crash.
13
+ # FATAL = 4
14
+ # An unknown message that should always be logged.
15
+ # UNKNOWN = 5
16
+
17
+ # Google Cloud severities:
18
+ # The log entry has no assigned severity level.
19
+ G_DEFAULT = 0
20
+
21
+ # Debug or trace information.
22
+ G_DEBUG = 100
23
+
24
+ # Routine information, such as ongoing status or performance.
25
+ G_INFO = 200
26
+
27
+ # Normal but significant events, such as start up, shut down, or a configuration change.
28
+ G_NOTICE = 300
29
+
30
+ # Warning events might cause problems.
31
+ G_WARNING = 400
32
+
33
+ # Error events are likely to cause problems.
34
+ G_ERROR = 500
35
+
36
+ # Critical events cause more severe problems or outages.
37
+ G_CRITICAL = 600
38
+
39
+ # A person must take an action immediately.
40
+ G_ALERT = 700
41
+
42
+ # One or more systems are unusable.
43
+ G_EMERGENCY = 800
44
+ end
45
+ end
46
+
47
+ module GoogleCloudRun
48
+ module Severity
49
+ include ::Logger::Severity
50
+
51
+ def self.to_s(severity)
52
+ case mapping(severity)
53
+ when G_DEFAULT; return "DEFAULT"
54
+ when G_DEBUG; return "DEBUG"
55
+ when G_INFO; return "INFO"
56
+ when G_NOTICE; return "NOTICE"
57
+ when G_WARNING; return "WARNING"
58
+ when G_ERROR; return "ERROR"
59
+ when G_CRITICAL; return "CRITICAL"
60
+ when G_ALERT; return "ALERT"
61
+ when G_EMERGENCY; return "EMERGENCY"
62
+ end
63
+ end
64
+
65
+ def self.mapping(severity)
66
+ case severity
67
+ when nil; return G_DEFAULT
68
+ when G_DEFAULT, G_DEBUG, G_INFO, G_NOTICE, G_WARNING, G_ERROR, G_CRITICAL, G_ALERT, G_EMERGENCY; return severity
69
+ when DEBUG; return G_DEBUG
70
+ when INFO; return G_INFO
71
+ when WARN; return G_WARNING
72
+ when ERROR; return G_ERROR
73
+ when FATAL; return G_CRITICAL
74
+ when UNKNOWN; return G_DEFAULT
75
+ when 0; return G_DEFAULT
76
+ when 1; return G_INFO
77
+ when 2; return G_WARNING
78
+ when 3; return G_ERROR
79
+ when 4; return G_CRITICAL
80
+ when 5; return G_DEFAULT
81
+ when 100; return G_DEBUG
82
+ when 200; return G_INFO
83
+ when 300; return G_NOTICE
84
+ when 400; return G_WARNING
85
+ when 500; return G_ERROR
86
+ when 600; return G_CRITICAL
87
+ when 700; return G_ALERT
88
+ when 800; return G_EMERGENCY
89
+ when "G_DEFAULT"; return G_DEFAULT
90
+ when "G_DEBUG"; return G_DEBUG
91
+ when "G_INFO"; return G_INFO
92
+ when "G_NOTICE"; return G_NOTICE
93
+ when "G_WARNING"; return G_WARNING
94
+ when "G_ERROR"; return G_ERROR
95
+ when "G_CRITICAL"; return G_CRITICAL
96
+ when "G_ALERT"; return G_ALERT
97
+ when "G_EMERGENCY"; return G_EMERGENCY
98
+ when :g_default; return G_DEFAULT
99
+ when :g_debug; return G_DEBUG
100
+ when :g_info; return G_INFO
101
+ when :g_notice; return G_NOTICE
102
+ when :g_warning; return G_WARNING
103
+ when :g_error; return G_ERROR
104
+ when :g_critical; return G_CRITICAL
105
+ when :g_alert; return G_ALERT
106
+ when :g_emergency; return G_EMERGENCY
107
+ when :debug; return G_DEBUG
108
+ when :info; return G_INFO
109
+ when :warn; return G_WARNING
110
+ when :error; return G_ERROR
111
+ when :fatal; return G_CRITICAL
112
+ when :unknown; return G_DEFAULT
113
+ when :default; return G_DEFAULT
114
+ when :notice; return G_NOTICE
115
+ when :warning; return G_WARNING
116
+ when :critical; return G_CRITICAL
117
+ when :alert; return G_ALERT
118
+ when :emergency; return G_EMERGENCY
119
+ else
120
+ raise "unknown severity '#{severity.inspect}'"
121
+ end
122
+ end
123
+ end
124
+ end
@@ -0,0 +1,103 @@
1
+ require "net/http"
2
+ require "uri"
3
+
4
+ module GoogleCloudRun
5
+ def self.k_service
6
+ @k_service ||= begin
7
+ ENV.fetch("K_SERVICE", "")
8
+ end
9
+ end
10
+
11
+ def self.k_revision
12
+ @k_revision ||= begin
13
+ revision = ENV.fetch("K_REVISION", "")
14
+ service = ENV.fetch("K_SERVICE", "")
15
+ revision.delete_prefix(service + "-")
16
+ end
17
+ end
18
+
19
+ # parse_trace_context parses header
20
+ # X-Cloud-Trace-Context: TRACE_ID/SPAN_ID;o=TRACE_TRUE
21
+ def self.parse_trace_context(raw)
22
+ raw&.strip!
23
+ return nil, nil, nil if raw.blank?
24
+
25
+ trace = nil
26
+ span = nil
27
+ sample = nil
28
+
29
+ first = raw.split("/")
30
+ if first.size > 0
31
+ trace = first[0]
32
+ end
33
+
34
+ if first.size > 1
35
+ second = first[1].split(";")
36
+
37
+ if second.size > 0
38
+ span = second[0]
39
+ end
40
+
41
+ if second.size > 1
42
+ case second[1].delete_prefix("o=")
43
+ when "1"; sample = true
44
+ when "0"; sample = false
45
+ end
46
+ end
47
+ end
48
+
49
+ return trace, span, sample
50
+ end
51
+
52
+ # project_id returns the current Google project id from the
53
+ # metadata server
54
+ def self.project_id
55
+ return "dummy-project" if Rails.env.test?
56
+
57
+ @project_id ||= begin
58
+ uri = URI.parse("http://metadata.google.internal/computeMetadata/v1/project/project-id")
59
+ request = Net::HTTP::Get.new(uri)
60
+ request["Metadata-Flavor"] = "Google"
61
+
62
+ req_options = {
63
+ open_timeout: 5,
64
+ read_timeout: 5,
65
+ max_retries: 2,
66
+ }
67
+
68
+ response = Net::HTTP.start(uri.hostname, uri.port, req_options) do |http|
69
+ http.request(request)
70
+ end
71
+
72
+ raise "unknown google cloud project" if response.code.to_i != 200
73
+
74
+ response.body.strip
75
+ end
76
+ end
77
+ #
78
+ # default_service_account_email returns the default service account's email from the
79
+ # metadata server
80
+ def self.default_service_account_email
81
+ return "123456789-compute@developer.gserviceaccount.com" if Rails.env.test?
82
+
83
+ @default_service_account_email ||= begin
84
+ uri = URI.parse("http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/email")
85
+ request = Net::HTTP::Get.new(uri)
86
+ request["Metadata-Flavor"] = "Google"
87
+
88
+ req_options = {
89
+ open_timeout: 5,
90
+ read_timeout: 5,
91
+ max_retries: 2,
92
+ }
93
+
94
+ response = Net::HTTP.start(uri.hostname, uri.port, req_options) do |http|
95
+ http.request(request)
96
+ end
97
+
98
+ raise "unknown google default service account" if response.code.to_i != 200
99
+
100
+ response.body.strip
101
+ end
102
+ end
103
+ end
@@ -0,0 +1,3 @@
1
+ module GoogleCloudRun
2
+ VERSION = "0.2.0"
3
+ end
@@ -0,0 +1,9 @@
1
+ require "google_cloud_run/severity"
2
+ require "google_cloud_run/util"
3
+ require "google_cloud_run/entry"
4
+ require "google_cloud_run/logger"
5
+ require "google_cloud_run/exceptions"
6
+ require "google_cloud_run/request_id"
7
+ require "google_cloud_run/engine"
8
+ require "google_cloud_run/job_adapter"
9
+ require "google_cloud_run/railtie"
metadata ADDED
@@ -0,0 +1,102 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: google_cloud_run
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.2.0
5
+ platform: ruby
6
+ authors:
7
+ - Matt Kadenbach
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2021-02-12 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rails
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '6.0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '6.0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: google-cloud-tasks
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '2.1'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '2.1'
41
+ - !ruby/object:Gem::Dependency
42
+ name: googleauth
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '0.15'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '0.15'
55
+ description: Opinionated Logging, Error Reporting and minor patches for Rails on Google
56
+ Cloud Run.
57
+ email:
58
+ executables: []
59
+ extensions: []
60
+ extra_rdoc_files: []
61
+ files:
62
+ - LICENSE
63
+ - README.md
64
+ - app/controllers/google_cloud_run/jobs_controller.rb
65
+ - config/routes.rb
66
+ - lib/google_cloud_run.rb
67
+ - lib/google_cloud_run/engine.rb
68
+ - lib/google_cloud_run/entry.rb
69
+ - lib/google_cloud_run/exceptions.rb
70
+ - lib/google_cloud_run/job_adapter.rb
71
+ - lib/google_cloud_run/logger.rb
72
+ - lib/google_cloud_run/railtie.rb
73
+ - lib/google_cloud_run/request_id.rb
74
+ - lib/google_cloud_run/severity.rb
75
+ - lib/google_cloud_run/util.rb
76
+ - lib/google_cloud_run/version.rb
77
+ homepage: https://github.com/mattes/google_cloud_run
78
+ licenses:
79
+ - MIT
80
+ metadata:
81
+ homepage_uri: https://github.com/mattes/google_cloud_run
82
+ source_code_uri: https://github.com/mattes/google_cloud_run
83
+ post_install_message:
84
+ rdoc_options: []
85
+ require_paths:
86
+ - lib
87
+ required_ruby_version: !ruby/object:Gem::Requirement
88
+ requirements:
89
+ - - ">="
90
+ - !ruby/object:Gem::Version
91
+ version: '0'
92
+ required_rubygems_version: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
97
+ requirements: []
98
+ rubygems_version: 3.2.3
99
+ signing_key:
100
+ specification_version: 4
101
+ summary: Rails on Google Cloud Run
102
+ test_files: []