connectors_service 8.5.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/LICENSE +93 -0
- data/NOTICE.txt +2 -0
- data/bin/connectors_service +4 -0
- data/bin/list_connectors +4 -0
- data/config/connectors.yml +25 -0
- data/lib/app/app.rb +25 -0
- data/lib/app/config.rb +132 -0
- data/lib/app/console_app.rb +278 -0
- data/lib/app/dispatcher.rb +121 -0
- data/lib/app/menu.rb +104 -0
- data/lib/app/preflight_check.rb +134 -0
- data/lib/app/version.rb +10 -0
- data/lib/connectors/base/adapter.rb +119 -0
- data/lib/connectors/base/connector.rb +57 -0
- data/lib/connectors/base/custom_client.rb +111 -0
- data/lib/connectors/connector_status.rb +31 -0
- data/lib/connectors/crawler/scheduler.rb +32 -0
- data/lib/connectors/example/connector.rb +57 -0
- data/lib/connectors/example/example_attachments/first_attachment.txt +1 -0
- data/lib/connectors/example/example_attachments/second_attachment.txt +1 -0
- data/lib/connectors/example/example_attachments/third_attachment.txt +1 -0
- data/lib/connectors/gitlab/adapter.rb +50 -0
- data/lib/connectors/gitlab/connector.rb +67 -0
- data/lib/connectors/gitlab/custom_client.rb +44 -0
- data/lib/connectors/gitlab/extractor.rb +69 -0
- data/lib/connectors/mongodb/connector.rb +138 -0
- data/lib/connectors/registry.rb +52 -0
- data/lib/connectors/sync_status.rb +21 -0
- data/lib/connectors.rb +16 -0
- data/lib/connectors_app/// +13 -0
- data/lib/connectors_service.rb +24 -0
- data/lib/connectors_utility.rb +16 -0
- data/lib/core/configuration.rb +48 -0
- data/lib/core/connector_settings.rb +142 -0
- data/lib/core/elastic_connector_actions.rb +269 -0
- data/lib/core/heartbeat.rb +32 -0
- data/lib/core/native_scheduler.rb +24 -0
- data/lib/core/output_sink/base_sink.rb +33 -0
- data/lib/core/output_sink/combined_sink.rb +38 -0
- data/lib/core/output_sink/console_sink.rb +51 -0
- data/lib/core/output_sink/es_sink.rb +74 -0
- data/lib/core/output_sink.rb +13 -0
- data/lib/core/scheduler.rb +158 -0
- data/lib/core/single_scheduler.rb +29 -0
- data/lib/core/sync_job_runner.rb +111 -0
- data/lib/core.rb +16 -0
- data/lib/list_connectors.rb +22 -0
- data/lib/stubs/app_config.rb +35 -0
- data/lib/stubs/connectors/stats.rb +35 -0
- data/lib/stubs/service_type.rb +13 -0
- data/lib/utility/constants.rb +20 -0
- data/lib/utility/cron.rb +81 -0
- data/lib/utility/elasticsearch/index/language_data.yml +111 -0
- data/lib/utility/elasticsearch/index/mappings.rb +104 -0
- data/lib/utility/elasticsearch/index/text_analysis_settings.rb +226 -0
- data/lib/utility/environment.rb +33 -0
- data/lib/utility/errors.rb +132 -0
- data/lib/utility/es_client.rb +84 -0
- data/lib/utility/exception_tracking.rb +64 -0
- data/lib/utility/extension_mapping_util.rb +123 -0
- data/lib/utility/logger.rb +84 -0
- data/lib/utility/middleware/basic_auth.rb +27 -0
- data/lib/utility/middleware/bearer_auth.rb +27 -0
- data/lib/utility/middleware/restrict_hostnames.rb +73 -0
- data/lib/utility.rb +16 -0
- 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
|