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 +7 -0
- data/LICENSE +20 -0
- data/README.md +200 -0
- data/app/controllers/google_cloud_run/jobs_controller.rb +32 -0
- data/config/routes.rb +3 -0
- data/lib/google_cloud_run/engine.rb +5 -0
- data/lib/google_cloud_run/entry.rb +153 -0
- data/lib/google_cloud_run/exceptions.rb +32 -0
- data/lib/google_cloud_run/job_adapter.rb +135 -0
- data/lib/google_cloud_run/logger.rb +316 -0
- data/lib/google_cloud_run/railtie.rb +60 -0
- data/lib/google_cloud_run/request_id.rb +16 -0
- data/lib/google_cloud_run/severity.rb +124 -0
- data/lib/google_cloud_run/util.rb +103 -0
- data/lib/google_cloud_run/version.rb +3 -0
- data/lib/google_cloud_run.rb +9 -0
- metadata +102 -0
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,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,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: []
|