Skip to content
Merged
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Allows the Elasticsearch sink to use [external versioning for documents](https://www.elastic.co/guide/en/elasticsearch/reference/current/docs-index_.html#index-versioning). To use it set `bulk.version_type` to `external` and then set `bulk.version` to either some static value like `123` or use templating to use an actual field from the document `{{ my_document_field }}`.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
Allows the Elasticsearch sink to use [external versioning for documents](https://www.elastic.co/guide/en/elasticsearch/reference/current/docs-index_.html#index-versioning). To use it set `bulk.version_type` to `external` and then set `bulk.version` to either some static value like `123` or use templating to use an actual field from the document `{{ my_document_field }}`.
Allows the Elasticsearch sink to use [external versioning for documents](https://www.elastic.co/guide/en/elasticsearch/reference/current/docs-index_.html#index-versioning). To use it, set `bulk.version_type` to `external` and then set `bulk.version` to either some static value like `123` or use templating to use an actual field from the document `{{ my_document_field }}`.


authors: radimsuckr
19 changes: 18 additions & 1 deletion src/sinks/elasticsearch/common.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ use vector_lib::config::LogNamespace;

use super::{
request_builder::ElasticsearchRequestBuilder, ElasticsearchApiVersion, ElasticsearchEncoder,
InvalidHostSnafu, Request,
InvalidHostSnafu, Request, VersionType,
};
use crate::{
http::{HttpClient, MaybeAuth},
Expand Down Expand Up @@ -93,6 +93,23 @@ impl ElasticsearchCommon {

let tower_request = config.request.tower.into_settings();

if config.bulk.version.is_some() && config.bulk.version_type == VersionType::Internal {
return Err(ParseError::ExternalVersionIgnoredWithInternalVersioning.into());
}
if config.bulk.version.is_some()
&& (config.bulk.version_type == VersionType::External
|| config.bulk.version_type == VersionType::ExternalGte)
&& config.id_key.is_none()
{
return Err(ParseError::ExternalVersioningWithoutDocumentID.into());
}
if config.bulk.version.is_none()
&& (config.bulk.version_type == VersionType::External
|| config.bulk.version_type == VersionType::ExternalGte)
{
return Err(ParseError::ExternalVersioningWithoutVersion.into());
}

let mut query_params = config.query.clone().unwrap_or_default();
query_params.insert(
"timeout".into(),
Expand Down
25 changes: 24 additions & 1 deletion src/sinks/elasticsearch/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ use crate::{
service::{ElasticsearchService, HttpRequestBuilder},
sink::ElasticsearchSink,
ElasticsearchApiVersion, ElasticsearchAuthConfig, ElasticsearchCommon,
ElasticsearchCommonMode, ElasticsearchMode,
ElasticsearchCommonMode, ElasticsearchMode, VersionType,
},
util::{
http::RequestConfig, service::HealthConfig, BatchConfig, Compression,
Expand Down Expand Up @@ -230,6 +230,8 @@ impl ElasticsearchConfig {
ElasticsearchMode::Bulk => Ok(ElasticsearchCommonMode::Bulk {
index: self.bulk.index.clone(),
action: self.bulk.action.clone(),
version: self.bulk.version.clone(),
version_type: self.bulk.version_type,
}),
ElasticsearchMode::DataStream => Ok(ElasticsearchCommonMode::DataStream(
self.data_stream.clone().unwrap_or_default(),
Expand Down Expand Up @@ -258,6 +260,21 @@ pub struct BulkConfig {
#[configurable(metadata(docs::examples = "application-{{ application_id }}-%Y-%m-%d"))]
#[configurable(metadata(docs::examples = "{{ index }}"))]
pub index: Template,

/// Version field value.
#[configurable(metadata(docs::examples = "{{ obj_version }}-%Y-%m-%d"))]
#[configurable(metadata(docs::examples = "123"))]
pub version: Option<Template>,

/// Version type.
///
/// Possible values are `internal`, `external` or `external_gt` and `external_gte`.
///
/// [es_index_versioning]: https://www.elastic.co/guide/en/elasticsearch/reference/current/docs-index_.html#index-versioning
#[serde(default = "default_version_type")]
#[configurable(metadata(docs::examples = "internal"))]
#[configurable(metadata(docs::examples = "external"))]
pub version_type: VersionType,
}

fn default_bulk_action() -> Template {
Expand All @@ -268,11 +285,17 @@ fn default_index() -> Template {
Template::try_from("vector-%Y.%m.%d").expect("unable to parse template")
}

const fn default_version_type() -> VersionType {
VersionType::Internal
}

impl Default for BulkConfig {
fn default() -> Self {
Self {
action: default_bulk_action(),
index: default_index(),
version: Default::default(),
version_type: default_version_type(),
}
}
}
Expand Down
105 changes: 90 additions & 15 deletions src/sinks/elasticsearch/encoder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,17 +13,45 @@ use crate::{
codecs::Transformer,
event::{EventFinalizers, Finalizable, LogEvent},
sinks::{
elasticsearch::BulkAction,
elasticsearch::{BulkAction, VersionType},
util::encoding::{as_tracked_write, Encoder},
},
};

#[derive(Serialize)]
pub enum DocumentVersionType {
External,
ExternalGte,
}

impl DocumentVersionType {
pub const fn as_str(&self) -> &'static str {
match self {
DocumentVersionType::External => VersionType::External.as_str(),
DocumentVersionType::ExternalGte => VersionType::ExternalGte.as_str(),
}
}
}

#[derive(Serialize)]
pub struct DocumentVersion {
pub kind: DocumentVersionType,
pub value: u64,
}

#[derive(Serialize)]
pub enum DocumentMetadata {
WithoutId,
Id(String),
IdAndVersion(String, DocumentVersion),
}

#[derive(Serialize)]
pub struct ProcessedEvent {
pub index: String,
pub bulk_action: BulkAction,
pub log: LogEvent,
pub id: Option<String>,
pub document_metadata: DocumentMetadata,
}

impl Finalizable for ProcessedEvent {
Expand All @@ -34,7 +62,14 @@ impl Finalizable for ProcessedEvent {

impl ByteSizeOf for ProcessedEvent {
fn allocated_bytes(&self) -> usize {
self.index.allocated_bytes() + self.log.allocated_bytes() + self.id.allocated_bytes()
match &self.document_metadata {
DocumentMetadata::WithoutId => {
self.index.allocated_bytes() + self.log.allocated_bytes()
}
DocumentMetadata::Id(id) | DocumentMetadata::IdAndVersion(id, _) => {
self.index.allocated_bytes() + self.log.allocated_bytes() + id.allocated_bytes()
}
}
}
}

Expand Down Expand Up @@ -86,7 +121,7 @@ impl Encoder<Vec<ProcessedEvent>> for ElasticsearchEncoder {
&event.index,
&self.doc_type,
self.suppress_type_name,
&event.id,
&event.document_metadata,
)?;
written_bytes +=
as_tracked_write::<_, _, io::Error>(writer, &log, |mut writer, log| {
Expand All @@ -107,36 +142,62 @@ fn write_bulk_action(
index: &str,
doc_type: &str,
suppress_type: bool,
id: &Option<String>,
document: &DocumentMetadata,
) -> std::io::Result<usize> {
as_tracked_write(
writer,
(bulk_action, index, doc_type, id, suppress_type),
|writer, (bulk_action, index, doc_type, id, suppress_type)| match (id, suppress_type) {
(Some(id), true) => {
(bulk_action, index, doc_type, suppress_type, document),
|writer, (bulk_action, index, doc_type, suppress_type, document)| match (
suppress_type,
document,
) {
(true, DocumentMetadata::Id(id)) => {
write!(
writer,
r#"{{"{}":{{"_index":"{}","_id":"{}"}}}}"#,
bulk_action, index, id
)
}
(Some(id), false) => {
(false, DocumentMetadata::Id(id)) => {
write!(
writer,
r#"{{"{}":{{"_index":"{}","_type":"{}","_id":"{}"}}}}"#,
bulk_action, index, doc_type, id
)
}
(None, true) => {
(true, DocumentMetadata::WithoutId) => {
write!(writer, r#"{{"{}":{{"_index":"{}"}}}}"#, bulk_action, index)
}
(None, false) => {
(false, DocumentMetadata::WithoutId) => {
write!(
writer,
r#"{{"{}":{{"_index":"{}","_type":"{}"}}}}"#,
bulk_action, index, doc_type
)
}
(true, DocumentMetadata::IdAndVersion(id, version)) => {
write!(
writer,
r#"{{"{}":{{"_index":"{}","_id":"{}","version_type":"{}","version":{}}}}}"#,
bulk_action,
index,
id,
version.kind.as_str(),
version.value
)
}
(false, DocumentMetadata::IdAndVersion(id, version)) => {
write!(
writer,
r#"{{"{}":{{"_index":"{}","_type":"{}","_id":"{}","version_type":"{}","version":{}}}}}"#,
bulk_action,
index,
doc_type,
id,
version.kind.as_str(),
version.value
)
}
},
)
}
Expand All @@ -155,7 +216,7 @@ mod tests {
"INDEX",
"TYPE",
true,
&Some("ID".to_string()),
&DocumentMetadata::Id("ID".to_string()),
);

let value: serde_json::Value = serde_json::from_slice(&writer).unwrap();
Expand All @@ -177,7 +238,14 @@ mod tests {
fn suppress_type_without_id() {
let mut writer = Vec::new();

_ = write_bulk_action(&mut writer, "ACTION", "INDEX", "TYPE", true, &None);
_ = write_bulk_action(
&mut writer,
"ACTION",
"INDEX",
"TYPE",
true,
&DocumentMetadata::WithoutId,
);

let value: serde_json::Value = serde_json::from_slice(&writer).unwrap();
let value = value.as_object().unwrap();
Expand All @@ -203,7 +271,7 @@ mod tests {
"INDEX",
"TYPE",
false,
&Some("ID".to_string()),
&DocumentMetadata::Id("ID".to_string()),
);

let value: serde_json::Value = serde_json::from_slice(&writer).unwrap();
Expand All @@ -226,7 +294,14 @@ mod tests {
fn type_without_id() {
let mut writer = Vec::new();

_ = write_bulk_action(&mut writer, "ACTION", "INDEX", "TYPE", false, &None);
_ = write_bulk_action(
&mut writer,
"ACTION",
"INDEX",
"TYPE",
false,
&DocumentMetadata::WithoutId,
);

let value: serde_json::Value = serde_json::from_slice(&writer).unwrap();
let value = value.as_object().unwrap();
Expand Down
Loading