google_cloud_run 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
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: []