connectors_service 8.5.0.1

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 (67) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +93 -0
  3. data/NOTICE.txt +2 -0
  4. data/bin/connectors_service +4 -0
  5. data/bin/list_connectors +4 -0
  6. data/config/connectors.yml +25 -0
  7. data/lib/app/app.rb +25 -0
  8. data/lib/app/config.rb +132 -0
  9. data/lib/app/console_app.rb +278 -0
  10. data/lib/app/dispatcher.rb +121 -0
  11. data/lib/app/menu.rb +104 -0
  12. data/lib/app/preflight_check.rb +134 -0
  13. data/lib/app/version.rb +10 -0
  14. data/lib/connectors/base/adapter.rb +119 -0
  15. data/lib/connectors/base/connector.rb +57 -0
  16. data/lib/connectors/base/custom_client.rb +111 -0
  17. data/lib/connectors/connector_status.rb +31 -0
  18. data/lib/connectors/crawler/scheduler.rb +32 -0
  19. data/lib/connectors/example/connector.rb +57 -0
  20. data/lib/connectors/example/example_attachments/first_attachment.txt +1 -0
  21. data/lib/connectors/example/example_attachments/second_attachment.txt +1 -0
  22. data/lib/connectors/example/example_attachments/third_attachment.txt +1 -0
  23. data/lib/connectors/gitlab/adapter.rb +50 -0
  24. data/lib/connectors/gitlab/connector.rb +67 -0
  25. data/lib/connectors/gitlab/custom_client.rb +44 -0
  26. data/lib/connectors/gitlab/extractor.rb +69 -0
  27. data/lib/connectors/mongodb/connector.rb +138 -0
  28. data/lib/connectors/registry.rb +52 -0
  29. data/lib/connectors/sync_status.rb +21 -0
  30. data/lib/connectors.rb +16 -0
  31. data/lib/connectors_app/// +13 -0
  32. data/lib/connectors_service.rb +24 -0
  33. data/lib/connectors_utility.rb +16 -0
  34. data/lib/core/configuration.rb +48 -0
  35. data/lib/core/connector_settings.rb +142 -0
  36. data/lib/core/elastic_connector_actions.rb +269 -0
  37. data/lib/core/heartbeat.rb +32 -0
  38. data/lib/core/native_scheduler.rb +24 -0
  39. data/lib/core/output_sink/base_sink.rb +33 -0
  40. data/lib/core/output_sink/combined_sink.rb +38 -0
  41. data/lib/core/output_sink/console_sink.rb +51 -0
  42. data/lib/core/output_sink/es_sink.rb +74 -0
  43. data/lib/core/output_sink.rb +13 -0
  44. data/lib/core/scheduler.rb +158 -0
  45. data/lib/core/single_scheduler.rb +29 -0
  46. data/lib/core/sync_job_runner.rb +111 -0
  47. data/lib/core.rb +16 -0
  48. data/lib/list_connectors.rb +22 -0
  49. data/lib/stubs/app_config.rb +35 -0
  50. data/lib/stubs/connectors/stats.rb +35 -0
  51. data/lib/stubs/service_type.rb +13 -0
  52. data/lib/utility/constants.rb +20 -0
  53. data/lib/utility/cron.rb +81 -0
  54. data/lib/utility/elasticsearch/index/language_data.yml +111 -0
  55. data/lib/utility/elasticsearch/index/mappings.rb +104 -0
  56. data/lib/utility/elasticsearch/index/text_analysis_settings.rb +226 -0
  57. data/lib/utility/environment.rb +33 -0
  58. data/lib/utility/errors.rb +132 -0
  59. data/lib/utility/es_client.rb +84 -0
  60. data/lib/utility/exception_tracking.rb +64 -0
  61. data/lib/utility/extension_mapping_util.rb +123 -0
  62. data/lib/utility/logger.rb +84 -0
  63. data/lib/utility/middleware/basic_auth.rb +27 -0
  64. data/lib/utility/middleware/bearer_auth.rb +27 -0
  65. data/lib/utility/middleware/restrict_hostnames.rb +73 -0
  66. data/lib/utility.rb +16 -0
  67. metadata +487 -0
@@ -0,0 +1,104 @@
1
+ #
2
+ # Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3
+ # or more contributor license agreements. Licensed under the Elastic License;
4
+ # you may not use this file except in compliance with the Elastic License.
5
+ #
6
+
7
+ # frozen_string_literal: true
8
+
9
+ module Utility
10
+ module Elasticsearch
11
+ module Index
12
+ module Mappings
13
+ ENUM_IGNORE_ABOVE = 2048
14
+
15
+ DATE_FIELD_MAPPING = {
16
+ type: 'date'
17
+ }
18
+
19
+ KEYWORD_FIELD_MAPPING = {
20
+ type: 'keyword'
21
+ }
22
+
23
+ TEXT_FIELD_MAPPING = {
24
+ type: 'text',
25
+ analyzer: 'iq_text_base',
26
+ index_options: 'freqs',
27
+ fields: {
28
+ 'stem': {
29
+ type: 'text',
30
+ analyzer: 'iq_text_stem'
31
+ },
32
+ 'prefix' => {
33
+ type: 'text',
34
+ analyzer: 'i_prefix',
35
+ search_analyzer: 'q_prefix',
36
+ index_options: 'docs'
37
+ },
38
+ 'delimiter' => {
39
+ type: 'text',
40
+ analyzer: 'iq_text_delimiter',
41
+ index_options: 'freqs'
42
+ },
43
+ 'joined': {
44
+ type: 'text',
45
+ analyzer: 'i_text_bigram',
46
+ search_analyzer: 'q_text_bigram',
47
+ index_options: 'freqs'
48
+ },
49
+ 'enum': {
50
+ type: 'keyword',
51
+ ignore_above: ENUM_IGNORE_ABOVE
52
+ }
53
+ }
54
+ }
55
+
56
+ WORKPLACE_SEARCH_SUBEXTRACTION_STAMP_FIELD_MAPPINGS = {
57
+ _subextracted_as_of: DATE_FIELD_MAPPING,
58
+ _subextracted_version: KEYWORD_FIELD_MAPPING
59
+ }.freeze
60
+
61
+ CRAWLER_FIELD_MAPPINGS = {
62
+ additional_urls: KEYWORD_FIELD_MAPPING,
63
+ body_content: TEXT_FIELD_MAPPING,
64
+ domains: KEYWORD_FIELD_MAPPING,
65
+ headings: TEXT_FIELD_MAPPING,
66
+ last_crawled_at: DATE_FIELD_MAPPING,
67
+ links: KEYWORD_FIELD_MAPPING,
68
+ meta_description: TEXT_FIELD_MAPPING,
69
+ meta_keywords: KEYWORD_FIELD_MAPPING,
70
+ title: TEXT_FIELD_MAPPING,
71
+ url: KEYWORD_FIELD_MAPPING,
72
+ url_host: KEYWORD_FIELD_MAPPING,
73
+ url_path: KEYWORD_FIELD_MAPPING,
74
+ url_path_dir1: KEYWORD_FIELD_MAPPING,
75
+ url_path_dir2: KEYWORD_FIELD_MAPPING,
76
+ url_path_dir3: KEYWORD_FIELD_MAPPING,
77
+ url_port: KEYWORD_FIELD_MAPPING,
78
+ url_scheme: KEYWORD_FIELD_MAPPING
79
+ }.freeze
80
+
81
+ def self.default_text_fields_mappings(connectors_index:, crawler_index: false)
82
+ {
83
+ dynamic: true,
84
+ dynamic_templates: [
85
+ {
86
+ data: {
87
+ match_mapping_type: 'string',
88
+ mapping: TEXT_FIELD_MAPPING
89
+ }
90
+ }
91
+ ],
92
+ properties: {
93
+ id: KEYWORD_FIELD_MAPPING
94
+ }.tap do |properties|
95
+ properties.merge!(WORKPLACE_SEARCH_SUBEXTRACTION_STAMP_FIELD_MAPPINGS) if connectors_index
96
+ end.tap do |properties|
97
+ properties.merge!(CRAWLER_FIELD_MAPPINGS) if crawler_index
98
+ end
99
+ }
100
+ end
101
+ end
102
+ end
103
+ end
104
+ end
@@ -0,0 +1,226 @@
1
+ #
2
+ # Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3
+ # or more contributor license agreements. Licensed under the Elastic License;
4
+ # you may not use this file except in compliance with the Elastic License.
5
+ #
6
+
7
+ # frozen_string_literal: true
8
+
9
+ require 'yaml'
10
+
11
+ module Utility
12
+ module Elasticsearch
13
+ module Index
14
+ class TextAnalysisSettings
15
+ class UnsupportedLanguageCode < StandardError; end
16
+
17
+ DEFAULT_LANGUAGE = :en
18
+ FRONT_NGRAM_MAX_GRAM = 12
19
+ LANGUAGE_DATA_FILE_PATH = File.join(File.dirname(__FILE__), 'language_data.yml')
20
+
21
+ GENERIC_FILTERS = {
22
+ front_ngram: {
23
+ type: 'edge_ngram',
24
+ min_gram: 1,
25
+ max_gram: FRONT_NGRAM_MAX_GRAM
26
+ },
27
+ delimiter: {
28
+ type: 'word_delimiter_graph',
29
+ generate_word_parts: true,
30
+ generate_number_parts: true,
31
+ catenate_words: true,
32
+ catenate_numbers: true,
33
+ catenate_all: true,
34
+ preserve_original: false,
35
+ split_on_case_change: true,
36
+ split_on_numerics: true,
37
+ stem_english_possessive: true
38
+ },
39
+ bigram_joiner: {
40
+ type: 'shingle',
41
+ token_separator: '',
42
+ max_shingle_size: 2,
43
+ output_unigrams: false
44
+ },
45
+ bigram_joiner_unigrams: {
46
+ type: 'shingle',
47
+ token_separator: '',
48
+ max_shingle_size: 2,
49
+ output_unigrams: true
50
+ },
51
+ bigram_max_size: {
52
+ type: 'length',
53
+ min: 0,
54
+ max: 16
55
+ }
56
+ }.freeze
57
+
58
+ NON_ICU_ANALYSIS_SETTINGS = {
59
+ tokenizer_name: 'standard', folding_filters: %w(cjk_width lowercase asciifolding)
60
+ }.freeze
61
+
62
+ ICU_ANALYSIS_SETTINGS = {
63
+ tokenizer_name: 'icu_tokenizer', folding_filters: %w(icu_folding)
64
+ }.freeze
65
+
66
+ def initialize(language_code: nil, analysis_icu: false)
67
+ @language_code = (language_code || DEFAULT_LANGUAGE).to_sym
68
+
69
+ raise UnsupportedLanguageCode, "Language '#{language_code}' is not supported" unless language_data[@language_code]
70
+
71
+ @analysis_icu = analysis_icu
72
+ @analysis_settings = icu_settings(analysis_icu)
73
+ end
74
+
75
+ def to_h
76
+ {
77
+ analysis: {
78
+ analyzer: analyzer_definitions,
79
+ filter: filter_definitions
80
+ },
81
+ index: {
82
+ similarity: {
83
+ default: {
84
+ type: 'BM25'
85
+ }
86
+ }
87
+ }
88
+ }
89
+ end
90
+
91
+ private
92
+
93
+ attr_reader :language_code, :analysis_settings
94
+
95
+ def icu_settings(analysis_settings)
96
+ return ICU_ANALYSIS_SETTINGS if analysis_settings
97
+
98
+ NON_ICU_ANALYSIS_SETTINGS
99
+ end
100
+
101
+ def stemmer_name
102
+ language_data[language_code][:stemmer]
103
+ end
104
+
105
+ def stop_words_name_or_list
106
+ language_data[language_code][:stop_words]
107
+ end
108
+
109
+ def custom_filter_definitions
110
+ language_data[language_code][:custom_filter_definitions] || {}
111
+ end
112
+
113
+ def prepended_filters
114
+ language_data[language_code][:prepended_filters] || []
115
+ end
116
+
117
+ def postpended_filters
118
+ language_data[language_code][:postpended_filters] || []
119
+ end
120
+
121
+ def stem_filter_name
122
+ "#{language_code}-stem-filter".to_sym
123
+ end
124
+
125
+ def stop_words_filter_name
126
+ "#{language_code}-stop-words-filter".to_sym
127
+ end
128
+
129
+ def filter_definitions
130
+ definitions = GENERIC_FILTERS.dup
131
+
132
+ definitions[stem_filter_name] = {
133
+ type: 'stemmer',
134
+ name: stemmer_name
135
+ }
136
+
137
+ definitions[stop_words_filter_name] = {
138
+ type: 'stop',
139
+ stopwords: stop_words_name_or_list
140
+ }
141
+
142
+ definitions.merge(custom_filter_definitions)
143
+ end
144
+
145
+ def analyzer_definitions
146
+ definitions = {}
147
+
148
+ definitions[:i_prefix] = {
149
+ tokenizer: analysis_settings[:tokenizer_name],
150
+ filter: [
151
+ *analysis_settings[:folding_filters],
152
+ 'front_ngram'
153
+ ]
154
+ }
155
+
156
+ definitions[:q_prefix] = {
157
+ tokenizer: analysis_settings[:tokenizer_name],
158
+ filter: [
159
+ *analysis_settings[:folding_filters]
160
+ ]
161
+ }
162
+
163
+ definitions[:iq_text_base] = {
164
+ tokenizer: analysis_settings[:tokenizer_name],
165
+ filter: [
166
+ *analysis_settings[:folding_filters],
167
+ stop_words_filter_name
168
+ ]
169
+ }
170
+
171
+ definitions[:iq_text_stem] = {
172
+ tokenizer: analysis_settings[:tokenizer_name],
173
+ filter: [
174
+ *prepended_filters,
175
+ *analysis_settings[:folding_filters],
176
+ stop_words_filter_name,
177
+ stem_filter_name,
178
+ *postpended_filters
179
+ ]
180
+ }
181
+
182
+ definitions[:iq_text_delimiter] = {
183
+ tokenizer: 'whitespace',
184
+ filter: [
185
+ *prepended_filters,
186
+ 'delimiter',
187
+ *analysis_settings[:folding_filters],
188
+ stop_words_filter_name,
189
+ stem_filter_name,
190
+ *postpended_filters
191
+ ]
192
+ }
193
+
194
+ definitions[:i_text_bigram] = {
195
+ tokenizer: analysis_settings[:tokenizer_name],
196
+ filter: [
197
+ *analysis_settings[:folding_filters],
198
+ stem_filter_name,
199
+ 'bigram_joiner',
200
+ 'bigram_max_size'
201
+ ]
202
+ }
203
+
204
+ definitions[:q_text_bigram] = {
205
+ tokenizer: analysis_settings[:tokenizer_name],
206
+ filter: [
207
+ *analysis_settings[:folding_filters],
208
+ stem_filter_name,
209
+ 'bigram_joiner_unigrams',
210
+ 'bigram_max_size'
211
+ ]
212
+ }
213
+
214
+ definitions
215
+ end
216
+
217
+ def language_data
218
+ @language_data ||= YAML.safe_load(
219
+ File.read(LANGUAGE_DATA_FILE_PATH),
220
+ symbolize_names: true
221
+ )
222
+ end
223
+ end
224
+ end
225
+ end
226
+ end
@@ -0,0 +1,33 @@
1
+ #
2
+ # Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3
+ # or more contributor license agreements. Licensed under the Elastic License;
4
+ # you may not use this file except in compliance with the Elastic License.
5
+ #
6
+
7
+ require 'logger'
8
+ require 'utility/logger'
9
+ require 'active_support/core_ext/module'
10
+
11
+ module Utility
12
+ module Environment
13
+ def self.set_execution_environment(config, &block)
14
+ # Set UTC as the timezone
15
+ ENV['TZ'] = 'UTC'
16
+ Logger.level = config[:log_level]
17
+ es_config = config[:elasticsearch]
18
+ disable_warnings = if es_config.has_key?(:disable_warnings)
19
+ es_config[:disable_warnings]
20
+ else
21
+ true
22
+ end
23
+
24
+ if disable_warnings
25
+ Logger.info('Disabling warnings')
26
+ Kernel.silence_warnings(&block)
27
+ else
28
+ Logger.info('Enabling warnings')
29
+ Kernel.enable_warnings(&block)
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,132 @@
1
+ #
2
+ # Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3
+ # or more contributor license agreements. Licensed under the Elastic License;
4
+ # you may not use this file except in compliance with the Elastic License.
5
+ #
6
+
7
+ require 'active_support/core_ext/string'
8
+
9
+ module Utility
10
+ class DocumentError
11
+ attr_accessor :error_class, :error_message, :stack_trace, :error_id
12
+
13
+ def initialize(error_class, error_message, stack_trace, error_id)
14
+ @error_class = error_class
15
+ @error_message = error_message
16
+ @error_id = error_id
17
+
18
+ # keywords must be < 32kb, UTF-8 chars can be up to 3 bytes, thus 32k/3 ~= 10k
19
+ # See https://github.com/elastic/workplace-search-team/issues/1723
20
+ @stack_trace = stack_trace.truncate(10_000)
21
+ end
22
+
23
+ def to_h
24
+ {
25
+ 'error_class' => error_class,
26
+ 'error_message' => error_message,
27
+ 'stack_trace' => stack_trace,
28
+ 'error_id' => error_id
29
+ }
30
+ end
31
+ end
32
+
33
+ class ClientError < StandardError; end
34
+ class EvictionWithNoProgressError < StandardError; end
35
+ class EvictionError < StandardError
36
+ attr_accessor :cursors
37
+
38
+ def initialize(message = nil, cursors: nil)
39
+ super(message)
40
+ @cursors = cursors
41
+ end
42
+ end
43
+
44
+ class SuspendedJobError < StandardError
45
+ attr_accessor :suspend_until, :cursors
46
+
47
+ def initialize(message = nil, suspend_until:, cursors: nil)
48
+ super(message)
49
+ @suspend_until = suspend_until
50
+ @cursors = cursors
51
+ end
52
+ end
53
+ class ThrottlingError < SuspendedJobError; end
54
+ class TransientServerError < SuspendedJobError; end
55
+ class UnrecoverableServerError < StandardError; end
56
+ class TransientSubextractorError < StandardError; end
57
+ class JobDocumentLimitError < StandardError; end
58
+ class JobClaimingError < StandardError; end
59
+
60
+ class MonitoringError < StandardError
61
+ attr_accessor :tripped_by
62
+
63
+ def initialize(message = nil, tripped_by: nil)
64
+ super("#{message}#{tripped_by.present? ? " Tripped by - #{tripped_by.class}: #{tripped_by.message}" : ''}")
65
+ @tripped_by = tripped_by
66
+ end
67
+ end
68
+ class MaxSuccessiveErrorsExceededError < MonitoringError; end
69
+ class MaxErrorsExceededError < MonitoringError; end
70
+ class MaxErrorsInWindowExceededError < MonitoringError; end
71
+
72
+ class JobSyncNotPossibleYetError < StandardError
73
+ attr_accessor :sync_will_be_possible_at
74
+
75
+ def initialize(message = nil, sync_will_be_possible_at: nil)
76
+ human_readable_errors = []
77
+
78
+ human_readable_errors.push(message) unless message.nil?
79
+ human_readable_errors.push("Content source was created too recently to schedule jobs, next job scheduling is possible at #{sync_will_be_possible_at}.") unless sync_will_be_possible_at.nil?
80
+
81
+ super(human_readable_errors.join(' '))
82
+ end
83
+ end
84
+ class PlatinumLicenseRequiredError < StandardError; end
85
+ class JobInterruptedError < StandardError; end
86
+ class JobCannotBeUpdatedError < StandardError; end
87
+ class SecretInvalidError < StandardError; end
88
+ class InvalidIndexingConfigurationError < StandardError; end
89
+ class InvalidTokenError < StandardError; end
90
+ class TokenRefreshFailedError < StandardError; end
91
+ class ConnectorNotAvailableError < StandardError; end
92
+
93
+ # For when we want to explicitly set a #cause but can't
94
+ class ExplicitlyCausedError < StandardError
95
+ attr_reader :reason
96
+
97
+ def initialize(reason)
98
+ @reason = reason
99
+ end
100
+ end
101
+
102
+ class PublishingFailedError < ExplicitlyCausedError; end
103
+
104
+ class Error
105
+ attr_reader :status_code, :code, :message
106
+
107
+ def initialize(status_code, code, message)
108
+ @status_code = status_code
109
+ @code = code
110
+ @message = message
111
+ end
112
+
113
+ def to_h
114
+ {
115
+ 'code' => @code,
116
+ 'message' => @message
117
+ }
118
+ end
119
+ end
120
+
121
+ class HealthCheckFailedError < StandardError
122
+ def initialize(msg = nil)
123
+ super("Health check failed for 3rd-party service: #{msg}")
124
+ end
125
+ end
126
+
127
+ INTERNAL_SERVER_ERROR = Utility::Error.new(500, 'INTERNAL_SERVER_ERROR', 'Internal server error')
128
+ INVALID_API_KEY = Utility::Error.new(401, 'INVALID_API_KEY', 'Invalid API key')
129
+ UNSUPPORTED_AUTH_SCHEME = Utility::Error.new(401, 'UNSUPPORTED_AUTH_SCHEME', 'Unsupported authorization scheme')
130
+ INVALID_ACCESS_TOKEN = Utility::Error.new(401, 'INVALID_ACCESS_TOKEN', 'Invalid/expired access token, please refresh the token')
131
+ TOKEN_REFRESH_ERROR = Utility::Error.new(401, 'TOKEN_REFRESH_ERROR', 'Failed to refresh token, please re-authenticate the application')
132
+ end
@@ -0,0 +1,84 @@
1
+ #
2
+ # Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3
+ # or more contributor license agreements. Licensed under the Elastic License;
4
+ # you may not use this file except in compliance with the Elastic License.
5
+ #
6
+
7
+ # frozen_string_literal: true
8
+
9
+ require 'logger'
10
+ require 'elasticsearch'
11
+
12
+ module Utility
13
+ class EsClient < ::Elasticsearch::Client
14
+ class IndexingFailedError < StandardError
15
+ def initialize(message, error = nil)
16
+ super(message)
17
+ @cause = error
18
+ end
19
+
20
+ attr_reader :cause
21
+ end
22
+
23
+ def initialize(es_config)
24
+ super(connection_configs(es_config))
25
+ end
26
+
27
+ def connection_configs(es_config)
28
+ configs = {}
29
+ configs[:api_key] = es_config[:api_key] if es_config[:api_key]
30
+ if es_config[:cloud_id]
31
+ configs[:cloud_id] = es_config[:cloud_id]
32
+ elsif es_config[:hosts]
33
+ configs[:hosts] = es_config[:hosts]
34
+ else
35
+ raise 'Either elasticsearch.cloud_id or elasticsearch.hosts should be configured.'
36
+ end
37
+ configs[:retry_on_failure] = es_config[:retry_on_failure] || false
38
+ configs[:request_timeout] = es_config[:request_timeout] || nil
39
+ configs[:log] = es_config[:log] || false
40
+ configs[:trace] = es_config[:trace] || false
41
+
42
+ # if log or trace is activated, we use the application logger
43
+ configs[:logger] = if configs[:log] || configs[:trace]
44
+ Utility::Logger.logger
45
+ else
46
+ # silence!
47
+ ::Logger.new(IO::NULL)
48
+ end
49
+ configs
50
+ end
51
+
52
+ def bulk(arguments = {})
53
+ raise_if_necessary(super(arguments))
54
+ end
55
+
56
+ private
57
+
58
+ def raise_if_necessary(response)
59
+ if response['errors']
60
+ first_error = nil
61
+
62
+ response['items'].each do |item|
63
+ %w[index delete].each do |op|
64
+ if item.has_key?(op) && item[op].has_key?('error')
65
+ first_error = item
66
+
67
+ break
68
+ end
69
+ end
70
+ end
71
+
72
+ if first_error
73
+ trace_id = Utility::Logger.generate_trace_id
74
+ Utility::Logger.error("Failed to index documents into Elasticsearch. First error in response is: #{first_error.to_json}")
75
+ short_message = Utility::Logger.abbreviated_message(first_error.to_json)
76
+ raise IndexingFailedError.new("Failed to index documents into Elasticsearch with an error '#{short_message}'. Look up the error ID [#{trace_id}] in the application logs to see the full error message.")
77
+ else
78
+ raise IndexingFailedError.new('Failed to index documents into Elasticsearch due to unknown error. Try enabling tracing for Elasticsearch and checking the logs.')
79
+ end
80
+ end
81
+ response
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,64 @@
1
+ #
2
+ # Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3
+ # or more contributor license agreements. Licensed under the Elastic License;
4
+ # you may not use this file except in compliance with the Elastic License.
5
+ #
6
+
7
+ # frozen_string_literal: true
8
+
9
+ require 'bson'
10
+ require 'utility/logger'
11
+
12
+ module Utility
13
+ class ExceptionTracking
14
+ class << self
15
+ def capture_message(message, context = {})
16
+ Utility::Logger.error("Error: #{message}. Context: #{context.inspect}")
17
+
18
+ # When the method is called from a rescue block, our return value may leak outside of its
19
+ # intended scope, so let's explicitly return nil here to be safe.
20
+ nil
21
+ end
22
+
23
+ def capture_exception(exception, context = {})
24
+ Utility::Logger.log_stacktrace(generate_stack_trace(exception))
25
+ Utility::Logger.error("Context: #{context.inspect}") if context
26
+ end
27
+
28
+ def log_exception(exception, message = nil)
29
+ Utility::Logger.error(message) if message
30
+ Utility::Logger.log_stacktrace(generate_stack_trace(exception))
31
+ end
32
+
33
+ def augment_exception(exception)
34
+ unless exception.respond_to?(:id)
35
+ exception.instance_eval do
36
+ def id
37
+ @error_id ||= BSON::ObjectId.new.to_s
38
+ end
39
+ end
40
+ end
41
+ end
42
+
43
+ def generate_error_message(exception, message, context)
44
+ context = { :message_id => exception.id }.merge(context || {}) if exception.respond_to?(:id)
45
+ context_message = context && "Context: #{context.inspect}"
46
+ ['Exception', message, exception.class.to_s, exception.message, context_message]
47
+ .compact
48
+ .map { |part| part.to_s.dup.force_encoding('UTF-8') }
49
+ .join(': ')
50
+ end
51
+
52
+ def generate_stack_trace(exception)
53
+ full_message = exception.full_message
54
+
55
+ cause = exception
56
+ while cause.cause != cause && (cause = cause.cause)
57
+ full_message << "Cause:\n#{cause.full_message}"
58
+ end
59
+
60
+ full_message.dup.force_encoding('UTF-8')
61
+ end
62
+ end
63
+ end
64
+ end