vector/sinks/elasticsearch/
mod.rs1mod common;
2mod config;
3pub mod encoder;
4pub mod health;
5pub mod request_builder;
6pub mod retry;
7pub mod service;
8pub mod sink;
9
10#[cfg(test)]
11mod tests;
12
13#[cfg(test)]
14#[cfg(feature = "es-integration-tests")]
15mod integration_tests;
16
17use std::{convert::TryFrom, fmt};
18
19pub use common::*;
20pub use config::*;
21pub use encoder::ElasticsearchEncoder;
22use http::{Request, uri::InvalidUri};
23use snafu::Snafu;
24use vector_lib::{
25 NamedInternalEvent, configurable::configurable_component, internal_event::InternalEvent,
26 sensitive_string::SensitiveString,
27};
28
29use crate::{
30 event::{EventRef, LogEvent},
31 internal_events::TemplateRenderingError,
32 template::{Template, TemplateParseError},
33};
34
35#[configurable_component]
37#[derive(Clone, Debug)]
38#[serde(deny_unknown_fields, rename_all = "snake_case", tag = "strategy")]
39#[configurable(metadata(
40 docs::enum_tag_description = "The authentication strategy to use.\n\nAmazon OpenSearch Serverless requires this option to be set to `aws`."
41))]
42pub enum ElasticsearchAuthConfig {
43 Basic {
45 #[configurable(metadata(docs::examples = "${ELASTICSEARCH_USERNAME}"))]
47 #[configurable(metadata(docs::examples = "username"))]
48 user: String,
49
50 #[configurable(metadata(docs::examples = "${ELASTICSEARCH_PASSWORD}"))]
52 #[configurable(metadata(docs::examples = "password"))]
53 password: SensitiveString,
54 },
55
56 #[cfg(feature = "aws-core")]
57 Aws(crate::aws::AwsAuthentication),
59}
60
61#[configurable_component]
63#[derive(Clone, Debug, Eq, PartialEq)]
64#[serde(deny_unknown_fields, rename_all = "snake_case")]
65#[derive(Default)]
66pub enum ElasticsearchMode {
67 #[serde(alias = "normal")]
69 #[default]
70 Bulk,
71
72 DataStream,
79}
80
81#[configurable_component]
83#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
84#[serde(deny_unknown_fields, rename_all = "snake_case")]
85pub enum BulkAction {
86 Index,
88
89 Create,
91
92 Update,
94}
95
96#[allow(clippy::trivially_copy_pass_by_ref)]
97impl BulkAction {
98 pub const fn as_str(&self) -> &'static str {
99 match self {
100 BulkAction::Index => "index",
101 BulkAction::Create => "create",
102 BulkAction::Update => "update",
103 }
104 }
105
106 pub const fn as_json_pointer(&self) -> &'static str {
107 match self {
108 BulkAction::Index => "/index",
109 BulkAction::Create => "/create",
110 BulkAction::Update => "/update",
111 }
112 }
113}
114
115impl TryFrom<&str> for BulkAction {
116 type Error = String;
117
118 fn try_from(input: &str) -> Result<Self, Self::Error> {
119 match input {
120 "index" => Ok(BulkAction::Index),
121 "create" => Ok(BulkAction::Create),
122 "update" => Ok(BulkAction::Update),
123 _ => Err(format!("Invalid bulk action: {input}")),
124 }
125 }
126}
127
128#[configurable_component]
130#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
131#[serde(deny_unknown_fields, rename_all = "snake_case")]
132pub enum VersionType {
133 Internal,
135
136 External,
138
139 ExternalGte,
141}
142
143#[allow(clippy::trivially_copy_pass_by_ref)]
144impl VersionType {
145 pub const fn as_str(&self) -> &'static str {
146 match self {
147 Self::Internal => "internal",
148 Self::External => "external",
149 Self::ExternalGte => "external_gte",
150 }
151 }
152}
153
154impl TryFrom<&str> for VersionType {
155 type Error = String;
156
157 fn try_from(input: &str) -> Result<Self, Self::Error> {
158 match input {
159 "internal" => Ok(VersionType::Internal),
160 "external" | "external_gt" => Ok(VersionType::External),
161 "external_gte" => Ok(VersionType::ExternalGte),
162 _ => Err(format!("Invalid versioning mode: {input}")),
163 }
164 }
165}
166
167impl_generate_config_from_default!(ElasticsearchConfig);
168
169#[derive(Debug, Clone)]
170pub enum ElasticsearchCommonMode {
171 Bulk {
172 index: Template,
173 template_fallback_index: Option<String>,
174 action: Template,
175 version: Option<Template>,
176 version_type: VersionType,
177 },
178 DataStream(DataStreamConfig),
179}
180
181#[derive(NamedInternalEvent)]
182struct VersionValueParseError<'a> {
183 value: &'a str,
184}
185
186impl InternalEvent for VersionValueParseError<'_> {
187 fn emit(self) {
188 warn!("{self}")
189 }
190}
191
192impl fmt::Display for VersionValueParseError<'_> {
193 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
194 write!(f, "Cannot parse version \"{}\" as integer", self.value)
195 }
196}
197
198impl ElasticsearchCommonMode {
199 fn index(&self, log: &LogEvent) -> Option<String> {
200 match self {
201 Self::Bulk {
202 index,
203 template_fallback_index,
204 ..
205 } => index
206 .render_string(log)
207 .or_else(|error| {
208 if matches!(
211 error,
212 crate::template::TemplateRenderingError::Confined { .. }
213 ) {
214 emit!(TemplateRenderingError {
215 error,
216 field: Some("index"),
217 drop_event: true,
218 });
219 return Err(());
220 }
221 if let Some(fallback) = template_fallback_index {
222 emit!(TemplateRenderingError {
223 error,
224 field: Some("index"),
225 drop_event: false,
226 });
227 Ok(fallback.clone())
228 } else {
229 emit!(TemplateRenderingError {
230 error,
231 field: Some("index"),
232 drop_event: true,
233 });
234 Err(())
235 }
236 })
237 .ok(),
238 Self::DataStream(ds) => ds.index(log),
239 }
240 }
241
242 fn bulk_action<'a>(&self, event: impl Into<EventRef<'a>>) -> Option<BulkAction> {
243 match self {
244 ElasticsearchCommonMode::Bulk {
245 action: bulk_action_template,
246 ..
247 } => bulk_action_template
248 .render_string(event)
249 .map_err(|error| {
250 emit!(TemplateRenderingError {
251 error,
252 field: Some("bulk_action"),
253 drop_event: true,
254 });
255 })
256 .ok()
257 .and_then(|value| BulkAction::try_from(value.as_str()).ok()),
258 ElasticsearchCommonMode::DataStream(_) => Some(BulkAction::Create),
260 }
261 }
262
263 fn version<'a>(&self, event: impl Into<EventRef<'a>>) -> Option<u64> {
264 match self {
265 ElasticsearchCommonMode::Bulk {
266 version: Some(version),
267 ..
268 } => version
269 .render_string(event)
270 .map_err(|error| {
271 emit!(TemplateRenderingError {
272 error,
273 field: Some("version"),
274 drop_event: true,
275 });
276 })
277 .ok()
278 .as_ref()
279 .and_then(|value| {
280 value
281 .parse()
282 .map_err(|_| emit!(VersionValueParseError { value }))
283 .ok()
284 }),
285 _ => None,
286 }
287 }
288
289 const fn version_type(&self) -> Option<VersionType> {
290 match self {
291 ElasticsearchCommonMode::Bulk { version_type, .. } => Some(*version_type),
292 _ => Some(VersionType::Internal),
293 }
294 }
295
296 const fn as_data_stream_config(&self) -> Option<&DataStreamConfig> {
297 match self {
298 Self::DataStream(value) => Some(value),
299 _ => None,
300 }
301 }
302}
303
304#[configurable_component]
306#[derive(Clone, Debug, Eq, PartialEq)]
307#[cfg_attr(feature = "proptest", derive(proptest_derive::Arbitrary))]
308#[serde(deny_unknown_fields, rename_all = "snake_case")]
309#[derive(Default)]
310pub enum ElasticsearchApiVersion {
311 #[default]
321 Auto,
322 V6,
324 V7,
326 V8,
328}
329
330#[derive(Debug, Snafu)]
331#[snafu(visibility(pub))]
332pub enum ParseError {
333 #[snafu(display("Invalid host {:?}: {:?}", host, source))]
334 InvalidHost { host: String, source: InvalidUri },
335 #[snafu(display("Host {:?} must include hostname", host))]
336 HostMustIncludeHostname { host: String },
337 #[snafu(display("Index template parse error: {}", source))]
338 IndexTemplate { source: TemplateParseError },
339 #[snafu(display("Batch action template parse error: {}", source))]
340 BatchActionTemplate { source: TemplateParseError },
341 #[cfg(feature = "aws-core")]
342 #[snafu(display("aws.region required when AWS authentication is in use"))]
343 RegionRequired,
344 #[snafu(display("Endpoints option must be specified"))]
345 EndpointRequired,
346 #[snafu(display(
347 "`endpoint` and `endpoints` options are mutually exclusive. Please use `endpoints` option."
348 ))]
349 EndpointsExclusive,
350 #[snafu(display("Tried to use external versioning without specifying the version itself"))]
351 ExternalVersioningWithoutVersion,
352 #[snafu(display("Cannot use external versioning without specifying a document ID"))]
353 ExternalVersioningWithoutDocumentID,
354 #[snafu(display("Your version field will be ignored because you use internal versioning"))]
355 ExternalVersionIgnoredWithInternalVersioning,
356 #[snafu(display("Amazon OpenSearch Serverless requires `api_version` value to be `auto`"))]
357 ServerlessElasticsearchApiVersionMustBeAuto,
358 #[snafu(display("Amazon OpenSearch Serverless requires `auth.strategy` value to be `aws`"))]
359 OpenSearchServerlessRequiresAwsAuth,
360}