connectors_service 8.5.0.1

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