aws_advanced_ruby_driver_wrapper 1.0.1 → 1.1.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 +4 -4
- data/CHANGELOG.md +32 -2
- data/README.md +115 -6
- data/THIRD-PARTY-LICENSES +70 -11
- data/aws_advanced_ruby_driver_wrapper.gemspec +9 -0
- data/lib/aws_advanced_ruby_driver_wrapper/active_record/aws_postgresql_adapter.rb +17 -3
- data/lib/aws_advanced_ruby_driver_wrapper/active_record/aws_trilogy_adapter.rb +80 -0
- data/lib/aws_advanced_ruby_driver_wrapper/active_record/type_adapter_alias.rb +1 -1
- data/lib/aws_advanced_ruby_driver_wrapper/driver_dialects/driver_dialect.rb +43 -0
- data/lib/aws_advanced_ruby_driver_wrapper/driver_dialects/driver_dialect_manager.rb +11 -4
- data/lib/aws_advanced_ruby_driver_wrapper/driver_dialects/mysql2_driver_dialect.rb +129 -0
- data/lib/aws_advanced_ruby_driver_wrapper/driver_dialects/mysql_driver_dialect.rb +9 -91
- data/lib/aws_advanced_ruby_driver_wrapper/driver_dialects/pg_driver_dialect.rb +18 -0
- data/lib/aws_advanced_ruby_driver_wrapper/driver_dialects/trilogy_driver_dialect.rb +118 -0
- data/lib/aws_advanced_ruby_driver_wrapper/errors/error_handler.rb +16 -0
- data/lib/aws_advanced_ruby_driver_wrapper/errors/mysql_error_handler.rb +10 -5
- data/lib/aws_advanced_ruby_driver_wrapper/errors/trilogy_error_handler.rb +127 -0
- data/lib/aws_advanced_ruby_driver_wrapper/errors.rb +1 -0
- data/lib/aws_advanced_ruby_driver_wrapper/monitoring/cluster_topology_monitor.rb +1 -0
- data/lib/aws_advanced_ruby_driver_wrapper/mysql.rb +27 -9
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/blue_green_plugin.rb +115 -7
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/drop_blue_connections_marker.rb +84 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/host_mapper.rb +3 -1
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/routing/base_routing.rb +7 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/routing/substitute_connect_routing.rb +68 -41
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/routing/suspend_connect_routing.rb +4 -2
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/routing/suspend_execute_routing.rb +5 -3
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/routing/suspend_until_corresponding_host_found_connect_routing.rb +7 -1
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/status_builder.rb +1 -1
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/status_monitor.rb +6 -13
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/status_provider.rb +90 -3
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/custom_endpoint/custom_endpoint_monitor.rb +11 -1
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/custom_endpoint/custom_endpoint_plugin.rb +4 -1
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/default_plugin.rb +14 -3
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/failover_plugin.rb +68 -3
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/federated_auth/base_saml_auth_plugin.rb +266 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/federated_auth/federated_auth_plugin.rb +134 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/federated_auth/okta_auth_plugin.rb +113 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/gdb/gdb_failover_plugin.rb +30 -9
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/iam_auth_plugin.rb +16 -2
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/initial_connection_strategy_plugin.rb +4 -1
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/kms_encryption/key_management_utility.rb +6 -3
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/kms_encryption/key_manager.rb +13 -4
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/kms_encryption/kms_encryption_plugin.rb +29 -6
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/kms_encryption/sql/trigger_mysql.sql +108 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/kms_encryption/sql/trigger_pg.sql +54 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/secrets_manager_plugin.rb +14 -0
- data/lib/aws_advanced_ruby_driver_wrapper/postgresql.rb +41 -23
- data/lib/aws_advanced_ruby_driver_wrapper/property_definition.rb +63 -1
- data/lib/aws_advanced_ruby_driver_wrapper/ruby_method.rb +3 -0
- data/lib/aws_advanced_ruby_driver_wrapper/services/dialect_service.rb +20 -7
- data/lib/aws_advanced_ruby_driver_wrapper/services/host_service.rb +34 -8
- data/lib/aws_advanced_ruby_driver_wrapper/services/monitor_service.rb +1 -1
- data/lib/aws_advanced_ruby_driver_wrapper/services/plugin_manager.rb +80 -29
- data/lib/aws_advanced_ruby_driver_wrapper/services/service_container.rb +10 -2
- data/lib/aws_advanced_ruby_driver_wrapper/services/service_utility.rb +4 -1
- data/lib/aws_advanced_ruby_driver_wrapper/trilogy.rb +167 -0
- data/lib/aws_advanced_ruby_driver_wrapper/utils/connection_config.rb +1 -1
- data/lib/aws_advanced_ruby_driver_wrapper/utils/connection_config_parser.rb +25 -15
- data/lib/aws_advanced_ruby_driver_wrapper/utils/global_aurora_topology_utils.rb +2 -2
- data/lib/aws_advanced_ruby_driver_wrapper/utils/iam_auth_utils.rb +13 -4
- data/lib/aws_advanced_ruby_driver_wrapper/utils/parser/encryption_annotation_parser.rb +15 -31
- data/lib/aws_advanced_ruby_driver_wrapper/utils/parser/mysql_statement_analyzer.rb +55 -76
- data/lib/aws_advanced_ruby_driver_wrapper/utils/parser/mysql_text.rb +215 -0
- data/lib/aws_advanced_ruby_driver_wrapper/utils/parser/pg_statement_analyzer.rb +11 -20
- data/lib/aws_advanced_ruby_driver_wrapper/utils/parser/query_analysis.rb +2 -3
- data/lib/aws_advanced_ruby_driver_wrapper/utils/parser/sql_parser.rb +21 -14
- data/lib/aws_advanced_ruby_driver_wrapper/utils/retry_util.rb +2 -1
- data/lib/aws_advanced_ruby_driver_wrapper/utils/saml_utils.rb +133 -0
- data/lib/aws_advanced_ruby_driver_wrapper/utils/storage/storage_service.rb +9 -5
- data/lib/aws_advanced_ruby_driver_wrapper/utils/telemetry/null_telemetry.rb +89 -0
- data/lib/aws_advanced_ruby_driver_wrapper/utils/telemetry/open_telemetry_context.rb +124 -0
- data/lib/aws_advanced_ruby_driver_wrapper/utils/telemetry/open_telemetry_counter.rb +39 -0
- data/lib/aws_advanced_ruby_driver_wrapper/utils/telemetry/open_telemetry_factory.rb +122 -0
- data/lib/aws_advanced_ruby_driver_wrapper/utils/telemetry/open_telemetry_gauge.rb +36 -0
- data/lib/aws_advanced_ruby_driver_wrapper/utils/telemetry/telemetry_const.rb +50 -0
- data/lib/aws_advanced_ruby_driver_wrapper/utils/telemetry/telemetry_factory.rb +143 -0
- data/lib/aws_advanced_ruby_driver_wrapper/utils/topology_utils.rb +9 -2
- data/lib/aws_advanced_ruby_driver_wrapper/version.rb +1 -1
- data/lib/aws_advanced_ruby_driver_wrapper.rb +9 -2
- metadata +54 -2
|
@@ -39,28 +39,34 @@ module AwsAdvancedRubyDriverWrapper
|
|
|
39
39
|
MAX_CACHE_ENTRIES = 1000
|
|
40
40
|
|
|
41
41
|
def initialize(driver_dialect)
|
|
42
|
-
@
|
|
42
|
+
@pg = pg_dialect?(driver_dialect)
|
|
43
|
+
@analyzer = resolve_analyzer
|
|
43
44
|
# A parser belongs to one connection, which is used by one thread at a time, so this plain
|
|
44
45
|
# hash needs no lock. Insertion order is used as the eviction order (least-recently-used).
|
|
45
46
|
@cache = {}
|
|
46
47
|
end
|
|
47
48
|
|
|
48
49
|
# Returns the analysis for +sql+, parsing it only on the first sight of a given statement and
|
|
49
|
-
# serving repeats from a bounded cache. The result is a pure function of the SQL
|
|
50
|
-
# parser's dialect, so caching by the
|
|
50
|
+
# serving repeats from a bounded cache. The result is a pure function of the SQL and the
|
|
51
|
+
# server version for this parser's dialect, so caching by the two of them is safe. The version
|
|
52
|
+
# is part of the key because a connection can reach a server of another version after a
|
|
53
|
+
# failover or a blue/green switchover.
|
|
51
54
|
#
|
|
55
|
+
# @param server_version [Integer, nil] the MySQL server's version, which decides whether a
|
|
56
|
+
# versioned +/*!+ comment is part of the statement; ignored on PostgreSQL
|
|
52
57
|
# @raise [StandardError] whatever the underlying analyzer raises on SQL it cannot read
|
|
53
|
-
def analyze_sql(sql)
|
|
58
|
+
def analyze_sql(sql, server_version: nil)
|
|
54
59
|
return empty_result unless sql.is_a?(String) && !sql.strip.empty?
|
|
55
60
|
|
|
56
|
-
|
|
61
|
+
key = [sql, server_version]
|
|
62
|
+
cached = @cache.delete(key)
|
|
57
63
|
unless cached.nil?
|
|
58
|
-
@cache[
|
|
64
|
+
@cache[key] = cached # re-insert so it counts as most-recently-used
|
|
59
65
|
return cached
|
|
60
66
|
end
|
|
61
67
|
|
|
62
|
-
result = build_analysis(sql)
|
|
63
|
-
@cache[
|
|
68
|
+
result = build_analysis(sql, server_version)
|
|
69
|
+
@cache[key] = result
|
|
64
70
|
@cache.shift if @cache.size > MAX_CACHE_ENTRIES # evict the least-recently-used entry
|
|
65
71
|
result
|
|
66
72
|
end
|
|
@@ -72,17 +78,18 @@ module AwsAdvancedRubyDriverWrapper
|
|
|
72
78
|
# the callers that must not write a value they cannot place look at the analysis itself.
|
|
73
79
|
#
|
|
74
80
|
# @param sql [String, nil]
|
|
81
|
+
# @param server_version [Integer, nil] see {#analyze_sql}
|
|
75
82
|
# @return [Hash{Integer => String}]
|
|
76
|
-
def column_parameter_mapping(sql)
|
|
77
|
-
analyze_sql(sql).parameter_column_names
|
|
83
|
+
def column_parameter_mapping(sql, server_version: nil)
|
|
84
|
+
analyze_sql(sql, server_version: server_version).parameter_column_names
|
|
78
85
|
rescue StandardError
|
|
79
86
|
{}
|
|
80
87
|
end
|
|
81
88
|
|
|
82
89
|
private
|
|
83
90
|
|
|
84
|
-
def build_analysis(sql)
|
|
85
|
-
analysis = @analyzer.analyze(sql)
|
|
91
|
+
def build_analysis(sql, server_version)
|
|
92
|
+
analysis = @pg ? @analyzer.analyze(sql) : @analyzer.analyze(sql, server_version: server_version)
|
|
86
93
|
SqlAnalysisResult.new(
|
|
87
94
|
query_type: analysis.query_type,
|
|
88
95
|
affected_tables: analysis.tables.to_set { |table_name| strip_schema_prefix(table_name) },
|
|
@@ -109,8 +116,8 @@ module AwsAdvancedRubyDriverWrapper
|
|
|
109
116
|
end
|
|
110
117
|
end
|
|
111
118
|
|
|
112
|
-
def resolve_analyzer
|
|
113
|
-
if
|
|
119
|
+
def resolve_analyzer
|
|
120
|
+
if @pg
|
|
114
121
|
require_relative 'pg_statement_analyzer'
|
|
115
122
|
# Loaded now rather than on the first statement, so a missing pg_query gem is reported
|
|
116
123
|
# when the parser is created instead of in the middle of a query.
|
|
@@ -137,7 +137,8 @@ module AwsAdvancedRubyDriverWrapper
|
|
|
137
137
|
|
|
138
138
|
def select_candidate(hosts, role, strategy)
|
|
139
139
|
@host_service.select_host(hosts, role, strategy)
|
|
140
|
-
rescue StandardError
|
|
140
|
+
rescue StandardError => e
|
|
141
|
+
logger.warn("Failed to select a host using the '#{strategy}' host selection strategy: #{e.message}")
|
|
141
142
|
nil
|
|
142
143
|
end
|
|
143
144
|
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
|
4
|
+
#
|
|
5
|
+
# Licensed under the Apache License, Version 2.0 (the "License").
|
|
6
|
+
# You may not use this file except in compliance with the License.
|
|
7
|
+
# You may obtain a copy of the License at
|
|
8
|
+
#
|
|
9
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
#
|
|
11
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
# See the License for the specific language governing permissions and
|
|
15
|
+
# limitations under the License.
|
|
16
|
+
|
|
17
|
+
require 'digest'
|
|
18
|
+
require 'net/http'
|
|
19
|
+
require 'openssl'
|
|
20
|
+
require 'uri'
|
|
21
|
+
require_relative '../errors'
|
|
22
|
+
|
|
23
|
+
module AwsAdvancedRubyDriverWrapper
|
|
24
|
+
module Utils
|
|
25
|
+
# Shared helpers for the SAML-based authentication plugins (AD FS, Okta):
|
|
26
|
+
# URL validation, the cache-key principal fingerprint, and a small Net::HTTP
|
|
27
|
+
# client used to talk to the identity provider's HTTP endpoints (there is no
|
|
28
|
+
# AWS SDK for these calls).
|
|
29
|
+
module SamlUtils
|
|
30
|
+
module_function
|
|
31
|
+
|
|
32
|
+
HTTPS_URL_PATTERN = %r{\Ahttps://[-a-zA-Z0-9+&@#/%?=~_!:,.']*[-a-zA-Z0-9+&@#/%=~_']}
|
|
33
|
+
|
|
34
|
+
# The regex the IdP sign-in response is scraped with for the SAML assertion,
|
|
35
|
+
# shared by the AD FS and Okta flows.
|
|
36
|
+
SAML_RESPONSE_PATTERN = /SAMLResponse\W+value="([^"]+)"/
|
|
37
|
+
|
|
38
|
+
# Rejects anything that is not a well-formed https:// URL, guarding the IdP
|
|
39
|
+
# requests against being redirected to a non-TLS or malformed endpoint.
|
|
40
|
+
#
|
|
41
|
+
# @raise [Errors::SamlAuthError] when the URL is not a valid https URL
|
|
42
|
+
def validate_url!(url)
|
|
43
|
+
uri = URI.parse(url)
|
|
44
|
+
return if uri.is_a?(URI::HTTPS) && url.match?(HTTPS_URL_PATTERN)
|
|
45
|
+
|
|
46
|
+
raise Errors::SamlAuthError, "Invalid HTTPS URL: #{url}"
|
|
47
|
+
rescue URI::InvalidURIError
|
|
48
|
+
raise Errors::SamlAuthError, "Invalid HTTPS URL: #{url}"
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# A short, stable fingerprint of the AWS principal a SAML connection assumes,
|
|
52
|
+
# for the token cache key. The principal is fully determined by the STS
|
|
53
|
+
# inputs iam_role_arn + iam_idp_arn, so two connections that assume different
|
|
54
|
+
# roles (or trust different identity providers) never share a cached token.
|
|
55
|
+
# No secret is included, and the federated username is intentionally excluded:
|
|
56
|
+
# once the role is assumed the database authority is the role.
|
|
57
|
+
#
|
|
58
|
+
# @param parts [Array<String, nil>] role_arn, idp_arn
|
|
59
|
+
# @return [String] 16 hex characters
|
|
60
|
+
def principal_fingerprint(*parts)
|
|
61
|
+
Digest::SHA256.hexdigest(parts.join("\0"))[0, 16]
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Performs an HTTP request against an IdP endpoint, following redirects and
|
|
65
|
+
# carrying cookies across them (AD FS and Okta both rely on this). Only
|
|
66
|
+
# https URLs are allowed; each hop is validated.
|
|
67
|
+
#
|
|
68
|
+
# @param method [Symbol] :get or :post
|
|
69
|
+
# @param url [String] the https URL to request
|
|
70
|
+
# @param body [String, nil] request body for :post
|
|
71
|
+
# @param headers [Hash] request headers
|
|
72
|
+
# @param ssl_insecure [Boolean] skip TLS verification when true
|
|
73
|
+
# @param open_timeout_sec [Float] connect timeout in seconds
|
|
74
|
+
# @param read_timeout_sec [Float] socket timeout in seconds
|
|
75
|
+
# @param cookies [Hash] a cookie jar, keyed by host then cookie name, shared across the
|
|
76
|
+
# calls of one sign-on flow and mutated in place. Cookies are only sent back to the host
|
|
77
|
+
# that set them, so following a redirect to another host cannot leak them.
|
|
78
|
+
# @return [Net::HTTPResponse]
|
|
79
|
+
def http_request(method:, url:, body: nil, headers: {}, ssl_insecure: false,
|
|
80
|
+
open_timeout_sec: 60.0, read_timeout_sec: 60.0, max_redirects: 3, cookies: {})
|
|
81
|
+
redirects = 0
|
|
82
|
+
loop do
|
|
83
|
+
validate_url!(url)
|
|
84
|
+
uri = URI.parse(url)
|
|
85
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
86
|
+
http.use_ssl = true
|
|
87
|
+
http.verify_mode = ssl_insecure ? OpenSSL::SSL::VERIFY_NONE : OpenSSL::SSL::VERIFY_PEER
|
|
88
|
+
http.open_timeout = open_timeout_sec
|
|
89
|
+
http.read_timeout = read_timeout_sec
|
|
90
|
+
|
|
91
|
+
request = build_request(method, uri, body, headers, cookies)
|
|
92
|
+
response = http.request(request)
|
|
93
|
+
merge_cookies!(cookies, uri.host, response)
|
|
94
|
+
|
|
95
|
+
if response.is_a?(Net::HTTPRedirection) && response['location'] && redirects < max_redirects
|
|
96
|
+
url = absolute_redirect(uri, response['location'])
|
|
97
|
+
method = :get
|
|
98
|
+
body = nil
|
|
99
|
+
redirects += 1
|
|
100
|
+
next
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
return response
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def build_request(method, uri, body, headers, cookies)
|
|
108
|
+
request =
|
|
109
|
+
case method
|
|
110
|
+
when :post then Net::HTTP::Post.new(uri)
|
|
111
|
+
else Net::HTTP::Get.new(uri)
|
|
112
|
+
end
|
|
113
|
+
headers.each { |k, v| request[k] = v }
|
|
114
|
+
request.body = body if body
|
|
115
|
+
host_jar = cookies[uri.host]
|
|
116
|
+
request['Cookie'] = host_jar.map { |k, v| "#{k}=#{v}" }.join('; ') if host_jar && !host_jar.empty?
|
|
117
|
+
request
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def merge_cookies!(cookies, host, response)
|
|
121
|
+
jar = (cookies[host] ||= {})
|
|
122
|
+
Array(response.get_fields('set-cookie')).each do |raw|
|
|
123
|
+
pair = raw.split(';', 2).first.to_s.split('=', 2)
|
|
124
|
+
jar[pair[0].strip] = pair[1] if pair.length == 2
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def absolute_redirect(uri, location)
|
|
129
|
+
URI.join(uri, location).to_s
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
end
|
|
@@ -63,9 +63,11 @@ module AwsAdvancedRubyDriverWrapper
|
|
|
63
63
|
# Retrieves an item. Returns nil if absent or expired.
|
|
64
64
|
# @param name [Symbol] registered cache name.
|
|
65
65
|
# @param key [Object] item key.
|
|
66
|
-
# @param register_access [Boolean] whether to publish a DataAccessEvent
|
|
66
|
+
# @param register_access [Boolean] whether to publish a DataAccessEvent, which extends the expiration of
|
|
67
|
+
# any monitor registered with this cache name as its produced_data_type. Pass true only when this read
|
|
68
|
+
# should keep that monitor alive.
|
|
67
69
|
# @return [Object, nil]
|
|
68
|
-
def get(name, key, register_access:
|
|
70
|
+
def get(name, key, register_access: false)
|
|
69
71
|
value = fetch_cache!(name).get(key)
|
|
70
72
|
return nil unless value
|
|
71
73
|
|
|
@@ -80,9 +82,11 @@ module AwsAdvancedRubyDriverWrapper
|
|
|
80
82
|
# plugin configuration (e.g. a plugin reading a cache owned by another, optional plugin).
|
|
81
83
|
# @param name [Symbol] cache name (need not be registered).
|
|
82
84
|
# @param key [Object] item key.
|
|
83
|
-
# @param register_access [Boolean] whether to publish a DataAccessEvent
|
|
85
|
+
# @param register_access [Boolean] whether to publish a DataAccessEvent, which extends the expiration of
|
|
86
|
+
# any monitor registered with this cache name as its produced_data_type. Pass true only when this read
|
|
87
|
+
# should keep that monitor alive.
|
|
84
88
|
# @return [Object, nil]
|
|
85
|
-
def get_if_registered(name, key, register_access:
|
|
89
|
+
def get_if_registered(name, key, register_access: false)
|
|
86
90
|
cache = @caches[name]
|
|
87
91
|
return nil if cache.nil?
|
|
88
92
|
|
|
@@ -99,7 +103,7 @@ module AwsAdvancedRubyDriverWrapper
|
|
|
99
103
|
# @param key [Object] item key.
|
|
100
104
|
# @return [Boolean]
|
|
101
105
|
def exists?(name, key)
|
|
102
|
-
!get(name, key
|
|
106
|
+
!get(name, key).nil?
|
|
103
107
|
end
|
|
104
108
|
|
|
105
109
|
# Removes an item.
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
|
4
|
+
#
|
|
5
|
+
# Licensed under the Apache License, Version 2.0 (the "License").
|
|
6
|
+
# You may not use this file except in compliance with the License.
|
|
7
|
+
# You may obtain a copy of the License at
|
|
8
|
+
#
|
|
9
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
#
|
|
11
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
# See the License for the specific language governing permissions and
|
|
15
|
+
# limitations under the License.
|
|
16
|
+
|
|
17
|
+
module AwsAdvancedRubyDriverWrapper
|
|
18
|
+
module Utils
|
|
19
|
+
module Telemetry
|
|
20
|
+
# Stands in for a context when no span is recorded, so callers never have to check for nil.
|
|
21
|
+
class NullTelemetryContext
|
|
22
|
+
INSTANCE = new.freeze
|
|
23
|
+
|
|
24
|
+
def name
|
|
25
|
+
'null'
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def success=(_success); end
|
|
29
|
+
|
|
30
|
+
def set_attribute(_key, _value); end
|
|
31
|
+
|
|
32
|
+
def record_exception(_exception); end
|
|
33
|
+
|
|
34
|
+
def close; end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
class NullTelemetryCounter
|
|
38
|
+
INSTANCE = new.freeze
|
|
39
|
+
|
|
40
|
+
def name
|
|
41
|
+
'null'
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def add(_value); end
|
|
45
|
+
|
|
46
|
+
def inc; end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
class NullTelemetryGauge
|
|
50
|
+
INSTANCE = new.freeze
|
|
51
|
+
|
|
52
|
+
def name
|
|
53
|
+
'null'
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# The factory used when telemetry is disabled.
|
|
58
|
+
class NullTelemetryFactory
|
|
59
|
+
def open_telemetry_context(_name, _trace_level)
|
|
60
|
+
NullTelemetryContext::INSTANCE
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def in_context(_name, _trace_level, record_result: false, record_failure: false)
|
|
64
|
+
yield NullTelemetryContext::INSTANCE
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def post_copy(_context, _trace_level); end
|
|
68
|
+
|
|
69
|
+
def create_counter(_name)
|
|
70
|
+
NullTelemetryCounter::INSTANCE
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def create_gauge(_name, &)
|
|
74
|
+
NullTelemetryGauge::INSTANCE
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def in_use?
|
|
78
|
+
false
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def traces_in_use?
|
|
82
|
+
false
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
INSTANCE = new.freeze
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
|
4
|
+
#
|
|
5
|
+
# Licensed under the Apache License, Version 2.0 (the "License").
|
|
6
|
+
# You may not use this file except in compliance with the License.
|
|
7
|
+
# You may obtain a copy of the License at
|
|
8
|
+
#
|
|
9
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
#
|
|
11
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
# See the License for the specific language governing permissions and
|
|
15
|
+
# limitations under the License.
|
|
16
|
+
|
|
17
|
+
require_relative '../../logging'
|
|
18
|
+
require_relative 'telemetry_const'
|
|
19
|
+
|
|
20
|
+
module AwsAdvancedRubyDriverWrapper
|
|
21
|
+
module Utils
|
|
22
|
+
module Telemetry
|
|
23
|
+
# A span, made the current one for as long as the context is open. Contexts must be closed in
|
|
24
|
+
# the reverse order they were opened, which {TelemetryFactory#in_context} guarantees.
|
|
25
|
+
class OpenTelemetryContext
|
|
26
|
+
include Logging
|
|
27
|
+
|
|
28
|
+
attr_reader :name
|
|
29
|
+
|
|
30
|
+
# @param start_timestamp [Time, nil] when the span started; now when nil
|
|
31
|
+
# @param links [Array<OpenTelemetry::Trace::Link>] spans the new span is linked to
|
|
32
|
+
def initialize(tracer, name, trace_level, start_timestamp: nil, links: [])
|
|
33
|
+
@name = name
|
|
34
|
+
@tracer = tracer
|
|
35
|
+
parent = OpenTelemetry::Trace.current_span.context
|
|
36
|
+
in_trace = parent.valid?
|
|
37
|
+
trace_level = TraceLevel::NO_TRACE if trace_level == TraceLevel::NESTED && !in_trace
|
|
38
|
+
|
|
39
|
+
case trace_level
|
|
40
|
+
when TraceLevel::FORCE_TOP_LEVEL, TraceLevel::TOP_LEVEL
|
|
41
|
+
# A top-level span starts a trace of its own. When there was a trace in progress, the new
|
|
42
|
+
# one is linked to it and records its ID, so the two can still be found together.
|
|
43
|
+
links += [OpenTelemetry::Trace::Link.new(parent)] if in_trace
|
|
44
|
+
@span = tracer.start_root_span(name, links: links.empty? ? nil : links, start_timestamp: start_timestamp)
|
|
45
|
+
set_attribute(TelemetryConst::PARENT_TRACE_ANNOTATION, parent.hex_trace_id) if in_trace
|
|
46
|
+
set_attribute(TelemetryConst::TRACE_NAME_ANNOTATION, name)
|
|
47
|
+
make_current
|
|
48
|
+
logger.debug { "[OTLP] Telemetry '#{name}' trace ID: #{@span.context.hex_trace_id}" }
|
|
49
|
+
when TraceLevel::NESTED
|
|
50
|
+
@span = tracer.start_span(name, links: links.empty? ? nil : links, start_timestamp: start_timestamp)
|
|
51
|
+
set_attribute(TelemetryConst::TRACE_NAME_ANNOTATION, name)
|
|
52
|
+
make_current
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def success=(success)
|
|
57
|
+
return if @span.nil?
|
|
58
|
+
|
|
59
|
+
@span.status = success ? OpenTelemetry::Trace::Status.ok : OpenTelemetry::Trace::Status.error
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def set_attribute(key, value)
|
|
63
|
+
@span&.set_attribute(key, value)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def record_exception(exception)
|
|
67
|
+
return if @span.nil? || exception.nil?
|
|
68
|
+
|
|
69
|
+
# The exception event below holds the same details, but span attributes can be filtered and
|
|
70
|
+
# searched on in trace backends, which events often cannot.
|
|
71
|
+
@span.set_attribute(TelemetryConst::EXCEPTION_TYPE_ANNOTATION, exception.class.name)
|
|
72
|
+
@span.set_attribute(TelemetryConst::EXCEPTION_MESSAGE_ANNOTATION, exception.message.to_s)
|
|
73
|
+
@span.record_exception(exception)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# @param end_timestamp [Time, nil] when the span ended; now when nil
|
|
77
|
+
def close(end_timestamp: nil)
|
|
78
|
+
return if @span.nil?
|
|
79
|
+
|
|
80
|
+
@span.finish(end_timestamp: end_timestamp)
|
|
81
|
+
OpenTelemetry::Context.detach(@token)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# Records a copy of this context's span at the given trace level: same start and end times,
|
|
85
|
+
# attributes and status, linked to the original and carrying its trace ID. Used to submit a
|
|
86
|
+
# span that was recorded inside another trace as a trace of its own as well.
|
|
87
|
+
#
|
|
88
|
+
# Does nothing when the span cannot be read back, as with the no-op spans the OpenTelemetry API
|
|
89
|
+
# hands out before an SDK is installed.
|
|
90
|
+
def record_copy(trace_level)
|
|
91
|
+
return if @span.nil? || !@span.respond_to?(:to_span_data)
|
|
92
|
+
|
|
93
|
+
original = @span.to_span_data
|
|
94
|
+
copy = OpenTelemetryContext.new(
|
|
95
|
+
@tracer, "#{TelemetryConst::COPY_TRACE_NAME_PREFIX}#{name}", trace_level,
|
|
96
|
+
start_timestamp: nanoseconds_to_time(original.start_timestamp), links: [OpenTelemetry::Trace::Link.new(@span.context)]
|
|
97
|
+
)
|
|
98
|
+
original.attributes&.each do |key, value|
|
|
99
|
+
copy.set_attribute(key, value) unless key == TelemetryConst::TRACE_NAME_ANNOTATION
|
|
100
|
+
end
|
|
101
|
+
copy.span&.status = original.status
|
|
102
|
+
copy.set_attribute(TelemetryConst::SOURCE_TRACE_ANNOTATION, original.hex_trace_id)
|
|
103
|
+
copy.close(end_timestamp: nanoseconds_to_time(original.end_timestamp))
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
protected
|
|
107
|
+
|
|
108
|
+
attr_reader :span
|
|
109
|
+
|
|
110
|
+
private
|
|
111
|
+
|
|
112
|
+
# Recorded spans report their timestamps as nanoseconds since the epoch, but new spans are given
|
|
113
|
+
# theirs as times.
|
|
114
|
+
def nanoseconds_to_time(nanoseconds)
|
|
115
|
+
nanoseconds && Time.at(0, nanoseconds, :nanosecond)
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def make_current
|
|
119
|
+
@token = OpenTelemetry::Context.attach(OpenTelemetry::Trace.context_with_span(@span))
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
|
4
|
+
#
|
|
5
|
+
# Licensed under the Apache License, Version 2.0 (the "License").
|
|
6
|
+
# You may not use this file except in compliance with the License.
|
|
7
|
+
# You may obtain a copy of the License at
|
|
8
|
+
#
|
|
9
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
#
|
|
11
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
# See the License for the specific language governing permissions and
|
|
15
|
+
# limitations under the License.
|
|
16
|
+
|
|
17
|
+
module AwsAdvancedRubyDriverWrapper
|
|
18
|
+
module Utils
|
|
19
|
+
module Telemetry
|
|
20
|
+
# A counter that only goes up.
|
|
21
|
+
class OpenTelemetryCounter
|
|
22
|
+
attr_reader :name
|
|
23
|
+
|
|
24
|
+
def initialize(meter, name)
|
|
25
|
+
@name = name
|
|
26
|
+
@counter = meter.create_counter(name)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def add(value)
|
|
30
|
+
@counter.add(value)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def inc
|
|
34
|
+
add(1)
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
|
4
|
+
#
|
|
5
|
+
# Licensed under the Apache License, Version 2.0 (the "License").
|
|
6
|
+
# You may not use this file except in compliance with the License.
|
|
7
|
+
# You may obtain a copy of the License at
|
|
8
|
+
#
|
|
9
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
#
|
|
11
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
# See the License for the specific language governing permissions and
|
|
15
|
+
# limitations under the License.
|
|
16
|
+
|
|
17
|
+
require_relative '../../version'
|
|
18
|
+
require_relative 'telemetry_const'
|
|
19
|
+
require_relative 'open_telemetry_context'
|
|
20
|
+
require_relative 'open_telemetry_counter'
|
|
21
|
+
require_relative 'open_telemetry_gauge'
|
|
22
|
+
|
|
23
|
+
module AwsAdvancedRubyDriverWrapper
|
|
24
|
+
module Utils
|
|
25
|
+
module Telemetry
|
|
26
|
+
# Records telemetry through the OpenTelemetry API. The application owns the SDK: it installs
|
|
27
|
+
# the tracer and meter providers and the exporters, and until it does, the API discards what is
|
|
28
|
+
# recorded here.
|
|
29
|
+
#
|
|
30
|
+
# The OpenTelemetry gems are optional dependencies of the wrapper, so they are only loaded
|
|
31
|
+
# when a backend that needs them is selected.
|
|
32
|
+
module OpenTelemetryFactory
|
|
33
|
+
NAME_MAX_LENGTH = 255
|
|
34
|
+
|
|
35
|
+
@gauges = {}
|
|
36
|
+
@gauges_lock = Mutex.new
|
|
37
|
+
|
|
38
|
+
class << self
|
|
39
|
+
def require_traces_api!
|
|
40
|
+
require 'opentelemetry-api'
|
|
41
|
+
rescue LoadError
|
|
42
|
+
raise LoadError,
|
|
43
|
+
"telemetry_traces_backend 'otlp' requires 'opentelemetry-api'. " \
|
|
44
|
+
"Add it to your Gemfile: gem 'opentelemetry-api' (or gem 'opentelemetry-sdk')"
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def require_metrics_api!
|
|
48
|
+
require 'opentelemetry-metrics-api'
|
|
49
|
+
begin
|
|
50
|
+
# Since opentelemetry-metrics-api 0.9.0, OpenTelemetry.meter_provider is only defined
|
|
51
|
+
# once this is required. Earlier versions define it as soon as the API is loaded.
|
|
52
|
+
require 'opentelemetry/metrics/global'
|
|
53
|
+
rescue LoadError
|
|
54
|
+
nil
|
|
55
|
+
end
|
|
56
|
+
rescue LoadError
|
|
57
|
+
raise LoadError,
|
|
58
|
+
"telemetry_metrics_backend 'otlp' requires 'opentelemetry-metrics-api'. " \
|
|
59
|
+
"Add it to your Gemfile: gem 'opentelemetry-metrics-api' (or gem 'opentelemetry-metrics-sdk')"
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def open_telemetry_context(name, trace_level)
|
|
63
|
+
OpenTelemetryContext.new(tracer, name, trace_level)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def post_copy(context, trace_level)
|
|
67
|
+
return if trace_level == TraceLevel::NO_TRACE || !context.is_a?(OpenTelemetryContext)
|
|
68
|
+
# A top-level copy must not find the trace in progress, or it would be linked to that trace
|
|
69
|
+
# as well as to the span it copies.
|
|
70
|
+
return context.record_copy(trace_level) if trace_level == TraceLevel::NESTED
|
|
71
|
+
|
|
72
|
+
OpenTelemetry::Context.with_current(OpenTelemetry::Context.empty) { context.record_copy(trace_level) }
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Unlike gauges, counters need no registry: in current versions of the metrics SDK (0.21 on),
|
|
76
|
+
# creating one again under the same name returns the instrument the meter already has, so every
|
|
77
|
+
# counter object adds to the same total.
|
|
78
|
+
def create_counter(name)
|
|
79
|
+
OpenTelemetryCounter.new(meter, trim(name))
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# There is one gauge per name for the whole process, since every connection records through
|
|
83
|
+
# the same meter. Creating a gauge that already exists replaces its callback, so the gauge
|
|
84
|
+
# reports the value of the callback set last.
|
|
85
|
+
def create_gauge(name, callback)
|
|
86
|
+
name = trim(name)
|
|
87
|
+
@gauges_lock.synchronize do
|
|
88
|
+
gauge = (@gauges[name] ||= OpenTelemetryGauge.new(meter, name))
|
|
89
|
+
gauge.callback = callback
|
|
90
|
+
gauge
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# Stops every gauge from reporting until it is created again: a gauge without a callback is
|
|
95
|
+
# skipped when metrics are collected. The gauges themselves stay registered, since
|
|
96
|
+
# registering one again under the same name would not take a new callback. For testing only.
|
|
97
|
+
#
|
|
98
|
+
# @api private
|
|
99
|
+
def clear_gauge_callbacks!
|
|
100
|
+
@gauges_lock.synchronize { @gauges.each_value { |gauge| gauge.callback = nil } }
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
private
|
|
104
|
+
|
|
105
|
+
# Both are safe to keep: until the application installs its SDK, the API hands out proxies
|
|
106
|
+
# that start delegating to the SDK once it is installed.
|
|
107
|
+
def tracer
|
|
108
|
+
@tracer ||= OpenTelemetry.tracer_provider.tracer(INSTRUMENTATION_NAME, VERSION)
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def meter
|
|
112
|
+
@meter ||= OpenTelemetry.meter_provider.meter(INSTRUMENTATION_NAME, version: VERSION)
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def trim(name)
|
|
116
|
+
name.length > NAME_MAX_LENGTH ? name[0, NAME_MAX_LENGTH] : name
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
|
4
|
+
#
|
|
5
|
+
# Licensed under the Apache License, Version 2.0 (the "License").
|
|
6
|
+
# You may not use this file except in compliance with the License.
|
|
7
|
+
# You may obtain a copy of the License at
|
|
8
|
+
#
|
|
9
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
#
|
|
11
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
# See the License for the specific language governing permissions and
|
|
15
|
+
# limitations under the License.
|
|
16
|
+
|
|
17
|
+
module AwsAdvancedRubyDriverWrapper
|
|
18
|
+
module Utils
|
|
19
|
+
module Telemetry
|
|
20
|
+
# A gauge whose value is read from its callback each time metrics are collected.
|
|
21
|
+
#
|
|
22
|
+
# The instrument is registered once, and reads whichever callback was set last. The callback
|
|
23
|
+
# can therefore be replaced, which a second registration of the same instrument would not do.
|
|
24
|
+
class OpenTelemetryGauge
|
|
25
|
+
attr_reader :name
|
|
26
|
+
attr_writer :callback
|
|
27
|
+
|
|
28
|
+
def initialize(meter, name)
|
|
29
|
+
@name = name
|
|
30
|
+
@callback = nil
|
|
31
|
+
@gauge = meter.create_observable_gauge(name, callback: -> { @callback&.call })
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|