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.
Files changed (81) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +32 -2
  3. data/README.md +115 -6
  4. data/THIRD-PARTY-LICENSES +70 -11
  5. data/aws_advanced_ruby_driver_wrapper.gemspec +9 -0
  6. data/lib/aws_advanced_ruby_driver_wrapper/active_record/aws_postgresql_adapter.rb +17 -3
  7. data/lib/aws_advanced_ruby_driver_wrapper/active_record/aws_trilogy_adapter.rb +80 -0
  8. data/lib/aws_advanced_ruby_driver_wrapper/active_record/type_adapter_alias.rb +1 -1
  9. data/lib/aws_advanced_ruby_driver_wrapper/driver_dialects/driver_dialect.rb +43 -0
  10. data/lib/aws_advanced_ruby_driver_wrapper/driver_dialects/driver_dialect_manager.rb +11 -4
  11. data/lib/aws_advanced_ruby_driver_wrapper/driver_dialects/mysql2_driver_dialect.rb +129 -0
  12. data/lib/aws_advanced_ruby_driver_wrapper/driver_dialects/mysql_driver_dialect.rb +9 -91
  13. data/lib/aws_advanced_ruby_driver_wrapper/driver_dialects/pg_driver_dialect.rb +18 -0
  14. data/lib/aws_advanced_ruby_driver_wrapper/driver_dialects/trilogy_driver_dialect.rb +118 -0
  15. data/lib/aws_advanced_ruby_driver_wrapper/errors/error_handler.rb +16 -0
  16. data/lib/aws_advanced_ruby_driver_wrapper/errors/mysql_error_handler.rb +10 -5
  17. data/lib/aws_advanced_ruby_driver_wrapper/errors/trilogy_error_handler.rb +127 -0
  18. data/lib/aws_advanced_ruby_driver_wrapper/errors.rb +1 -0
  19. data/lib/aws_advanced_ruby_driver_wrapper/monitoring/cluster_topology_monitor.rb +1 -0
  20. data/lib/aws_advanced_ruby_driver_wrapper/mysql.rb +27 -9
  21. data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/blue_green_plugin.rb +115 -7
  22. data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/drop_blue_connections_marker.rb +84 -0
  23. data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/host_mapper.rb +3 -1
  24. data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/routing/base_routing.rb +7 -0
  25. data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/routing/substitute_connect_routing.rb +68 -41
  26. data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/routing/suspend_connect_routing.rb +4 -2
  27. data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/routing/suspend_execute_routing.rb +5 -3
  28. data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/routing/suspend_until_corresponding_host_found_connect_routing.rb +7 -1
  29. data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/status_builder.rb +1 -1
  30. data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/status_monitor.rb +6 -13
  31. data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/status_provider.rb +90 -3
  32. data/lib/aws_advanced_ruby_driver_wrapper/plugins/custom_endpoint/custom_endpoint_monitor.rb +11 -1
  33. data/lib/aws_advanced_ruby_driver_wrapper/plugins/custom_endpoint/custom_endpoint_plugin.rb +4 -1
  34. data/lib/aws_advanced_ruby_driver_wrapper/plugins/default_plugin.rb +14 -3
  35. data/lib/aws_advanced_ruby_driver_wrapper/plugins/failover_plugin.rb +68 -3
  36. data/lib/aws_advanced_ruby_driver_wrapper/plugins/federated_auth/base_saml_auth_plugin.rb +266 -0
  37. data/lib/aws_advanced_ruby_driver_wrapper/plugins/federated_auth/federated_auth_plugin.rb +134 -0
  38. data/lib/aws_advanced_ruby_driver_wrapper/plugins/federated_auth/okta_auth_plugin.rb +113 -0
  39. data/lib/aws_advanced_ruby_driver_wrapper/plugins/gdb/gdb_failover_plugin.rb +30 -9
  40. data/lib/aws_advanced_ruby_driver_wrapper/plugins/iam_auth_plugin.rb +16 -2
  41. data/lib/aws_advanced_ruby_driver_wrapper/plugins/initial_connection_strategy_plugin.rb +4 -1
  42. data/lib/aws_advanced_ruby_driver_wrapper/plugins/kms_encryption/key_management_utility.rb +6 -3
  43. data/lib/aws_advanced_ruby_driver_wrapper/plugins/kms_encryption/key_manager.rb +13 -4
  44. data/lib/aws_advanced_ruby_driver_wrapper/plugins/kms_encryption/kms_encryption_plugin.rb +29 -6
  45. data/lib/aws_advanced_ruby_driver_wrapper/plugins/kms_encryption/sql/trigger_mysql.sql +108 -0
  46. data/lib/aws_advanced_ruby_driver_wrapper/plugins/kms_encryption/sql/trigger_pg.sql +54 -0
  47. data/lib/aws_advanced_ruby_driver_wrapper/plugins/secrets_manager_plugin.rb +14 -0
  48. data/lib/aws_advanced_ruby_driver_wrapper/postgresql.rb +41 -23
  49. data/lib/aws_advanced_ruby_driver_wrapper/property_definition.rb +63 -1
  50. data/lib/aws_advanced_ruby_driver_wrapper/ruby_method.rb +3 -0
  51. data/lib/aws_advanced_ruby_driver_wrapper/services/dialect_service.rb +20 -7
  52. data/lib/aws_advanced_ruby_driver_wrapper/services/host_service.rb +34 -8
  53. data/lib/aws_advanced_ruby_driver_wrapper/services/monitor_service.rb +1 -1
  54. data/lib/aws_advanced_ruby_driver_wrapper/services/plugin_manager.rb +80 -29
  55. data/lib/aws_advanced_ruby_driver_wrapper/services/service_container.rb +10 -2
  56. data/lib/aws_advanced_ruby_driver_wrapper/services/service_utility.rb +4 -1
  57. data/lib/aws_advanced_ruby_driver_wrapper/trilogy.rb +167 -0
  58. data/lib/aws_advanced_ruby_driver_wrapper/utils/connection_config.rb +1 -1
  59. data/lib/aws_advanced_ruby_driver_wrapper/utils/connection_config_parser.rb +25 -15
  60. data/lib/aws_advanced_ruby_driver_wrapper/utils/global_aurora_topology_utils.rb +2 -2
  61. data/lib/aws_advanced_ruby_driver_wrapper/utils/iam_auth_utils.rb +13 -4
  62. data/lib/aws_advanced_ruby_driver_wrapper/utils/parser/encryption_annotation_parser.rb +15 -31
  63. data/lib/aws_advanced_ruby_driver_wrapper/utils/parser/mysql_statement_analyzer.rb +55 -76
  64. data/lib/aws_advanced_ruby_driver_wrapper/utils/parser/mysql_text.rb +215 -0
  65. data/lib/aws_advanced_ruby_driver_wrapper/utils/parser/pg_statement_analyzer.rb +11 -20
  66. data/lib/aws_advanced_ruby_driver_wrapper/utils/parser/query_analysis.rb +2 -3
  67. data/lib/aws_advanced_ruby_driver_wrapper/utils/parser/sql_parser.rb +21 -14
  68. data/lib/aws_advanced_ruby_driver_wrapper/utils/retry_util.rb +2 -1
  69. data/lib/aws_advanced_ruby_driver_wrapper/utils/saml_utils.rb +133 -0
  70. data/lib/aws_advanced_ruby_driver_wrapper/utils/storage/storage_service.rb +9 -5
  71. data/lib/aws_advanced_ruby_driver_wrapper/utils/telemetry/null_telemetry.rb +89 -0
  72. data/lib/aws_advanced_ruby_driver_wrapper/utils/telemetry/open_telemetry_context.rb +124 -0
  73. data/lib/aws_advanced_ruby_driver_wrapper/utils/telemetry/open_telemetry_counter.rb +39 -0
  74. data/lib/aws_advanced_ruby_driver_wrapper/utils/telemetry/open_telemetry_factory.rb +122 -0
  75. data/lib/aws_advanced_ruby_driver_wrapper/utils/telemetry/open_telemetry_gauge.rb +36 -0
  76. data/lib/aws_advanced_ruby_driver_wrapper/utils/telemetry/telemetry_const.rb +50 -0
  77. data/lib/aws_advanced_ruby_driver_wrapper/utils/telemetry/telemetry_factory.rb +143 -0
  78. data/lib/aws_advanced_ruby_driver_wrapper/utils/topology_utils.rb +9 -2
  79. data/lib/aws_advanced_ruby_driver_wrapper/version.rb +1 -1
  80. data/lib/aws_advanced_ruby_driver_wrapper.rb +9 -2
  81. metadata +54 -2
@@ -39,28 +39,34 @@ module AwsAdvancedRubyDriverWrapper
39
39
  MAX_CACHE_ENTRIES = 1000
40
40
 
41
41
  def initialize(driver_dialect)
42
- @analyzer = resolve_analyzer(driver_dialect)
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 for this
50
- # parser's dialect, so caching by the SQL string is safe.
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
- cached = @cache.delete(sql)
61
+ key = [sql, server_version]
62
+ cached = @cache.delete(key)
57
63
  unless cached.nil?
58
- @cache[sql] = cached # re-insert so it counts as most-recently-used
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[sql] = result
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(driver_dialect)
113
- if pg_dialect?(driver_dialect)
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: true)
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: true)
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, register_access: false).nil?
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