Skip to main content

vector/sinks/util/buffer/
compression.rs

1use std::{cell::RefCell, collections::BTreeSet, fmt};
2
3use indexmap::IndexMap;
4use serde::{de, ser};
5use serde_json::Value;
6use vector_lib::configurable::{
7    Configurable, GenerateError, Metadata, ToValue,
8    attributes::CustomAttribute,
9    schema::{
10        SchemaGenerator, SchemaObject, apply_metadata, generate_const_string_schema,
11        generate_enum_schema, generate_one_of_schema, generate_struct_schema,
12        get_or_generate_schema,
13    },
14};
15
16use crate::sinks::util::zstd::ZstdCompressionLevel;
17
18/// Compression configuration.
19#[derive(Copy, Clone, Debug, Default, Eq, PartialEq)]
20pub enum Compression {
21    /// No compression.
22    #[default]
23    None,
24
25    /// [Gzip][gzip] compression.
26    ///
27    /// [gzip]: https://www.gzip.org/
28    Gzip(CompressionLevel),
29
30    /// [Zlib][zlib] compression.
31    ///
32    /// [zlib]: https://zlib.net/
33    Zlib(CompressionLevel),
34
35    /// [Zstandard][zstd] compression.
36    ///
37    /// [zstd]: https://facebook.github.io/zstd/
38    Zstd(CompressionLevel),
39
40    /// [Snappy][snappy] compression.
41    ///
42    /// [snappy]: https://github.com/google/snappy/blob/main/docs/README.md
43    Snappy,
44}
45
46impl Compression {
47    /// Gets whether or not this compression will actually compress the input.
48    ///
49    /// While it may be counterintuitive for "compression" to not compress, this is simply a
50    /// consequence of designing a single type that may or may not compress so that we can avoid
51    /// having to box writers at a higher-level.
52    ///
53    /// Some callers can benefit from knowing whether or not compression is actually taking place,
54    /// as different size limitations may come into play.
55    pub const fn is_compressed(&self) -> bool {
56        !matches!(self, Compression::None)
57    }
58
59    pub const fn gzip_default() -> Compression {
60        Compression::Gzip(CompressionLevel::const_default())
61    }
62
63    pub const fn zlib_default() -> Compression {
64        Compression::Zlib(CompressionLevel::const_default())
65    }
66
67    pub const fn zstd_default() -> Compression {
68        Compression::Zstd(CompressionLevel::const_default())
69    }
70
71    pub const fn content_encoding(self) -> Option<&'static str> {
72        match self {
73            Self::None => None,
74            Self::Gzip(_) => Some("gzip"),
75            Self::Zlib(_) => Some("deflate"),
76            Self::Zstd(_) => Some("zstd"),
77            Self::Snappy => Some("snappy"),
78        }
79    }
80
81    pub const fn accept_encoding(self) -> Option<&'static str> {
82        match self {
83            Self::Gzip(_) => Some("gzip"),
84            Self::Zlib(_) => Some("deflate"),
85            Self::Zstd(_) => Some("zstd"),
86            Self::Snappy => Some("snappy"),
87            _ => None,
88        }
89    }
90
91    pub const fn extension(self) -> &'static str {
92        match self {
93            Self::None => "log",
94            Self::Gzip(_) => "log.gz",
95            Self::Zlib(_) => "log.zz",
96            Self::Zstd(_) => "log.zst",
97            Self::Snappy => "log.snappy",
98        }
99    }
100
101    pub const fn max_compression_level_val(self) -> u32 {
102        match self {
103            Compression::None => 0,
104            Compression::Gzip(_) => 9,
105            Compression::Zlib(_) => 9,
106            Compression::Zstd(_) => 21,
107            Compression::Snappy => 0,
108        }
109    }
110
111    pub const fn compression_level(self) -> CompressionLevel {
112        match self {
113            Self::None | Self::Snappy => CompressionLevel::None,
114            Self::Gzip(level) | Self::Zlib(level) | Self::Zstd(level) => level,
115        }
116    }
117}
118
119impl fmt::Display for Compression {
120    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
121        match *self {
122            Compression::None => write!(f, "none"),
123            Compression::Gzip(ref level) => write!(f, "gzip({})", level.as_flate2().level()),
124            Compression::Zlib(ref level) => write!(f, "zlib({})", level.as_flate2().level()),
125            Compression::Zstd(ref level) => {
126                write!(f, "zstd({})", ZstdCompressionLevel::from(*level))
127            }
128            Compression::Snappy => write!(f, "snappy"),
129        }
130    }
131}
132
133impl<'de> de::Deserialize<'de> for Compression {
134    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
135    where
136        D: de::Deserializer<'de>,
137    {
138        struct StringOrMap;
139
140        impl<'de> de::Visitor<'de> for StringOrMap {
141            type Value = Compression;
142
143            fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
144                f.write_str("string or map")
145            }
146
147            fn visit_str<E>(self, s: &str) -> Result<Self::Value, E>
148            where
149                E: de::Error,
150            {
151                match s {
152                    "none" => Ok(Compression::None),
153                    "gzip" => Ok(Compression::gzip_default()),
154                    "zlib" => Ok(Compression::zlib_default()),
155                    "zstd" => Ok(Compression::zstd_default()),
156                    "snappy" => Ok(Compression::Snappy),
157                    _ => Err(de::Error::invalid_value(
158                        de::Unexpected::Str(s),
159                        &r#""none" or "gzip" or "zlib" or "zstd""#,
160                    )),
161                }
162            }
163
164            fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
165            where
166                A: de::MapAccess<'de>,
167            {
168                let mut algorithm = None;
169                let mut level = None;
170
171                while let Some(key) = map.next_key::<String>()? {
172                    match key.as_str() {
173                        "algorithm" => {
174                            if algorithm.is_some() {
175                                return Err(de::Error::duplicate_field("algorithm"));
176                            }
177                            algorithm = Some(map.next_value::<String>()?);
178                        }
179                        "level" => {
180                            if level.is_some() {
181                                return Err(de::Error::duplicate_field("level"));
182                            }
183                            level = Some(map.next_value::<CompressionLevel>()?);
184                        }
185                        _ => return Err(de::Error::unknown_field(&key, &["algorithm", "level"])),
186                    };
187                }
188
189                let compression = match algorithm
190                    .ok_or_else(|| de::Error::missing_field("algorithm"))?
191                    .as_str()
192                {
193                    "none" => match level {
194                        Some(_) => Err(de::Error::unknown_field("level", &[])),
195                        None => Ok(Compression::None),
196                    },
197                    "gzip" => Ok(Compression::Gzip(level.unwrap_or_default())),
198                    "zlib" => Ok(Compression::Zlib(level.unwrap_or_default())),
199                    "zstd" => Ok(Compression::Zstd(level.unwrap_or_default())),
200                    "snappy" => match level {
201                        Some(_) => Err(de::Error::unknown_field("level", &[])),
202                        None => Ok(Compression::Snappy),
203                    },
204                    algorithm => Err(de::Error::unknown_variant(
205                        algorithm,
206                        &["none", "gzip", "zlib", "zstd", "snappy"],
207                    )),
208                }?;
209
210                if let CompressionLevel::Val(level) = compression.compression_level() {
211                    let max_level = compression.max_compression_level_val();
212                    if level > max_level {
213                        let msg = std::format!(
214                            "invalid value `{level}`, expected value in range [0, {max_level}]"
215                        );
216                        return Err(de::Error::custom(msg));
217                    }
218                }
219
220                Ok(compression)
221            }
222        }
223
224        deserializer.deserialize_any(StringOrMap)
225    }
226}
227
228impl ser::Serialize for Compression {
229    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
230    where
231        S: ser::Serializer,
232    {
233        use ser::SerializeMap;
234
235        match self {
236            Compression::None => serializer.serialize_str("none"),
237            Compression::Gzip(gzip_level) => {
238                if *gzip_level != CompressionLevel::Default {
239                    let mut map = serializer.serialize_map(None)?;
240                    map.serialize_entry("algorithm", "gzip")?;
241                    map.serialize_entry("level", &gzip_level)?;
242                    map.end()
243                } else {
244                    serializer.serialize_str("gzip")
245                }
246            }
247            Compression::Zlib(zlib_level) => {
248                if *zlib_level != CompressionLevel::Default {
249                    let mut map = serializer.serialize_map(None)?;
250                    map.serialize_entry("algorithm", "zlib")?;
251                    map.serialize_entry("level", &zlib_level)?;
252                    map.end()
253                } else {
254                    serializer.serialize_str("zlib")
255                }
256            }
257            Compression::Zstd(zstd_level) => {
258                if *zstd_level != CompressionLevel::Default {
259                    let mut map = serializer.serialize_map(None)?;
260                    map.serialize_entry("algorithm", "zstd")?;
261                    map.serialize_entry("level", &zstd_level)?;
262                    map.end()
263                } else {
264                    serializer.serialize_str("zstd")
265                }
266            }
267            Compression::Snappy => serializer.serialize_str("snappy"),
268        }
269    }
270}
271
272pub const ALGORITHM_NAME: &str = "algorithm";
273pub const LEVEL_NAME: &str = "level";
274pub const LOGICAL_NAME: &str = "logical_name";
275pub const ENUM_TAGGING_MODE: &str = "docs::enum_tagging";
276
277pub fn generate_string_schema(
278    logical_name: &str,
279    title: Option<&'static str>,
280    description: &'static str,
281) -> SchemaObject {
282    let mut const_schema = generate_const_string_schema(logical_name.to_lowercase());
283    let mut const_metadata = Metadata::with_description(description);
284    if let Some(title) = title {
285        const_metadata.set_title(title);
286    }
287    const_metadata.add_custom_attribute(CustomAttribute::kv(LOGICAL_NAME, logical_name));
288    apply_metadata(&mut const_schema, const_metadata);
289    const_schema
290}
291
292// TODO: Consider an approach for generating schema of "string or object" structure used by this type.
293impl Configurable for Compression {
294    fn referenceable_name() -> Option<&'static str> {
295        Some(std::any::type_name::<Self>())
296    }
297
298    fn metadata() -> Metadata {
299        let mut metadata = Metadata::default();
300        metadata.set_title("Compression configuration.");
301        metadata.set_description("All compression algorithms use the default compression level unless otherwise specified.");
302        metadata.add_custom_attribute(CustomAttribute::kv("docs::enum_tagging", "external"));
303        metadata
304    }
305
306    fn generate_schema(
307        generator: &RefCell<SchemaGenerator>,
308    ) -> Result<SchemaObject, GenerateError> {
309        // First, we'll create the string-only subschemas for each algorithm, and wrap those up
310        // within a one-of schema.
311        let mut string_metadata = Metadata::with_description("Compression algorithm.");
312        string_metadata.add_custom_attribute(CustomAttribute::kv(ENUM_TAGGING_MODE, "external"));
313
314        let none_string_subschema = generate_string_schema("None", None, "No compression.");
315        let gzip_string_subschema = generate_string_schema(
316            "Gzip",
317            Some("[Gzip][gzip] compression."),
318            "[gzip]: https://www.gzip.org/",
319        );
320        let zlib_string_subschema = generate_string_schema(
321            "Zlib",
322            Some("[Zlib][zlib] compression."),
323            "[zlib]: https://zlib.net/",
324        );
325
326        let zstd_string_subschema = generate_string_schema(
327            "Zstd",
328            Some("[Zstandard][zstd] compression."),
329            "[zstd]: https://facebook.github.io/zstd/",
330        );
331
332        let snappy_string_subschema = generate_string_schema(
333            "Snappy",
334            Some("[Snappy][snappy] compression."),
335            "[snappy]: https://github.com/google/snappy/blob/main/docs/README.md",
336        );
337
338        let mut all_string_oneof_subschema = generate_one_of_schema(&[
339            none_string_subschema,
340            gzip_string_subschema,
341            zlib_string_subschema,
342            zstd_string_subschema,
343            snappy_string_subschema,
344        ]);
345        apply_metadata(&mut all_string_oneof_subschema, string_metadata);
346
347        // Next we'll create a full schema for the given algorithms.
348        //
349        // TODO: We're currently using all three algorithms in the enum subschema for `algorithm`,
350        // but in reality, `level` is never used when the algorithm is `none`. This is _currently_
351        // fine because the field is optional, and we don't use `deny_unknown_fields`, so if users
352        // specify it when the algorithm is `none`: no harm, no foul.
353        //
354        // However, it does lead to a suboptimal schema being generated, one that sort of implies it
355        // may have value when set, even if the algorithm is `none`. We do this because, otherwise,
356        // it's very hard to reconcile the resolved schemas during component documentation
357        // generation, where we need to be able to generate the right enum key/value pair for the
358        // `none` algorithm as part of the overall set of enum values declared for the `algorithm`
359        // field in the "full" schema version.
360        let compression_level_schema =
361            get_or_generate_schema(&CompressionLevel::as_configurable_ref(), generator, None)?;
362
363        let mut required = BTreeSet::new();
364        required.insert(ALGORITHM_NAME.to_string());
365
366        let mut properties = IndexMap::new();
367        properties.insert(
368            ALGORITHM_NAME.to_string(),
369            all_string_oneof_subschema.clone(),
370        );
371        properties.insert(LEVEL_NAME.to_string(), compression_level_schema);
372
373        let mut full_subschema = generate_struct_schema(properties, required, None);
374        let mut full_metadata =
375            Metadata::with_description("Compression algorithm and compression level.");
376        full_metadata.add_custom_attribute(CustomAttribute::flag("docs::hidden"));
377        apply_metadata(&mut full_subschema, full_metadata);
378
379        // Finally, we zip both schemas together.
380        Ok(generate_one_of_schema(&[
381            all_string_oneof_subschema,
382            full_subschema,
383        ]))
384    }
385}
386
387impl ToValue for Compression {
388    fn to_value(&self) -> Value {
389        serde_json::to_value(self).expect("Could not convert compression settings to JSON")
390    }
391}
392
393/// Compression level.
394#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
395pub enum CompressionLevel {
396    None,
397    #[default]
398    Default,
399    Best,
400    Fast,
401    Val(u32),
402}
403
404impl CompressionLevel {
405    pub const fn const_default() -> Self {
406        CompressionLevel::Default
407    }
408
409    pub fn as_flate2(self) -> flate2::Compression {
410        match self {
411            CompressionLevel::None => flate2::Compression::none(),
412            CompressionLevel::Default => flate2::Compression::default(),
413            CompressionLevel::Best => flate2::Compression::best(),
414            CompressionLevel::Fast => flate2::Compression::fast(),
415            CompressionLevel::Val(level) => flate2::Compression::new(level),
416        }
417    }
418}
419
420impl<'de> de::Deserialize<'de> for CompressionLevel {
421    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
422    where
423        D: de::Deserializer<'de>,
424    {
425        struct NumberOrString;
426
427        impl de::Visitor<'_> for NumberOrString {
428            type Value = CompressionLevel;
429
430            fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
431                f.write_str("unsigned number or string")
432            }
433
434            fn visit_str<E>(self, s: &str) -> Result<Self::Value, E>
435            where
436                E: de::Error,
437            {
438                match s {
439                    "none" => Ok(CompressionLevel::None),
440                    "fast" => Ok(CompressionLevel::Fast),
441                    "default" => Ok(CompressionLevel::Default),
442                    "best" => Ok(CompressionLevel::Best),
443                    level => Err(de::Error::invalid_value(
444                        de::Unexpected::Str(level),
445                        &r#""none", "fast", "best" or "default""#,
446                    )),
447                }
448            }
449
450            fn visit_u64<E>(self, v: u64) -> Result<Self::Value, E>
451            where
452                E: de::Error,
453            {
454                u32::try_from(v).map(CompressionLevel::Val).map_err(|err| {
455                    de::Error::custom(format!(
456                        "unsigned integer could not be converted to u32: {err}"
457                    ))
458                })
459            }
460
461            fn visit_i64<E>(self, v: i64) -> Result<Self::Value, E>
462            where
463                E: de::Error,
464            {
465                u32::try_from(v).map(CompressionLevel::Val).map_err(|err| {
466                    de::Error::custom(format!("integer could not be converted to u32: {err}"))
467                })
468            }
469        }
470
471        deserializer.deserialize_any(NumberOrString)
472    }
473}
474
475impl ser::Serialize for CompressionLevel {
476    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
477    where
478        S: ser::Serializer,
479    {
480        match *self {
481            CompressionLevel::None => serializer.serialize_str("none"),
482            CompressionLevel::Default => serializer.serialize_str("default"),
483            CompressionLevel::Best => serializer.serialize_str("best"),
484            CompressionLevel::Fast => serializer.serialize_str("fast"),
485            CompressionLevel::Val(level) => serializer.serialize_u64(u64::from(level)),
486        }
487    }
488}
489
490// TODO: Consider an approach for generating schema of "string or number" structure used by this type.
491impl Configurable for CompressionLevel {
492    fn referenceable_name() -> Option<&'static str> {
493        Some(std::any::type_name::<Self>())
494    }
495
496    fn metadata() -> Metadata {
497        let mut metadata = Metadata::default();
498        metadata.set_description("Compression level.");
499        metadata
500    }
501
502    fn generate_schema(_: &RefCell<SchemaGenerator>) -> Result<SchemaObject, GenerateError> {
503        let string_consts = ["none", "fast", "best", "default"]
504            .iter()
505            .map(|s| serde_json::Value::from(*s));
506
507        let level_consts = (0u32..=21).map(serde_json::Value::from);
508
509        let valid_values = string_consts.chain(level_consts).collect();
510        Ok(generate_enum_schema(valid_values))
511    }
512}
513
514impl ToValue for CompressionLevel {
515    fn to_value(&self) -> Value {
516        // FIXME
517        serde_json::to_value(self).expect("Could not convert compression level to JSON")
518    }
519}
520
521#[cfg(test)]
522mod test {
523    use super::{Compression, CompressionLevel};
524
525    #[test]
526    fn deserialization_json() {
527        let fixtures_valid = [
528            (r#""none""#, Compression::None),
529            (r#""gzip""#, Compression::Gzip(CompressionLevel::default())),
530            (r#""zlib""#, Compression::Zlib(CompressionLevel::default())),
531            (r#""snappy""#, Compression::Snappy),
532            (r#"{"algorithm": "none"}"#, Compression::None),
533            (
534                r#"{"algorithm": "gzip"}"#,
535                Compression::Gzip(CompressionLevel::default()),
536            ),
537            (
538                r#"{"algorithm": "gzip", "level": "best"}"#,
539                Compression::Gzip(CompressionLevel::Best),
540            ),
541            (
542                r#"{"algorithm": "gzip", "level": 8}"#,
543                Compression::Gzip(CompressionLevel::Val(8)),
544            ),
545            (
546                r#"{"algorithm": "zlib"}"#,
547                Compression::Zlib(CompressionLevel::default()),
548            ),
549            (
550                r#"{"algorithm": "zlib", "level": "best"}"#,
551                Compression::Zlib(CompressionLevel::Best),
552            ),
553            (
554                r#"{"algorithm": "zlib", "level": 8}"#,
555                Compression::Zlib(CompressionLevel::Val(8)),
556            ),
557        ];
558        for (sources, result) in fixtures_valid.iter() {
559            let deserialized: Result<Compression, _> = serde_json::from_str(sources);
560            assert_eq!(deserialized.expect("valid source"), *result);
561        }
562
563        let fixtures_invalid = [
564            (
565                r"42",
566                r"invalid type: integer `42`, expected string or map at line 1 column 2",
567            ),
568            (
569                r#""b42""#,
570                r#"invalid value: string "b42", expected "none" or "gzip" or "zlib" or "zstd" at line 1 column 5"#,
571            ),
572            (
573                r#"{"algorithm": "b42"}"#,
574                r"unknown variant `b42`, expected one of `none`, `gzip`, `zlib`, `zstd`, `snappy` at line 1 column 20",
575            ),
576            (
577                r#"{"algorithm": "none", "level": "default"}"#,
578                r"unknown field `level`, there are no fields at line 1 column 41",
579            ),
580            (
581                r#"{"algorithm": "gzip", "level": -1}"#,
582                r"integer could not be converted to u32: out of range integral type conversion attempted at line 1 column 33",
583            ),
584            (
585                r#"{"algorithm": "gzip", "level": "good"}"#,
586                r#"invalid value: string "good", expected "none", "fast", "best" or "default" at line 1 column 37"#,
587            ),
588            (
589                r#"{"algorithm": "gzip", "level": {}}"#,
590                r"invalid type: map, expected unsigned number or string at line 1 column 33",
591            ),
592            (
593                r#"{"algorithm": "gzip", "level": "default", "key": 42}"#,
594                r"unknown field `key`, expected `algorithm` or `level` at line 1 column 47",
595            ),
596            (
597                r#"{"algorithm": "gzip", "level": 10}"#,
598                r"invalid value `10`, expected value in range [0, 9] at line 1 column 34",
599            ),
600            (
601                r#"{"algorithm": "zstd", "level": 22}"#,
602                r"invalid value `22`, expected value in range [0, 21] at line 1 column 34",
603            ),
604            (
605                r#"{"algorithm": "snappy", "level": 3}"#,
606                r"unknown field `level`, there are no fields at line 1 column 35",
607            ),
608        ];
609        for (source, result) in fixtures_invalid.iter() {
610            let deserialized: Result<Compression, _> = serde_json::from_str(source);
611            let error = deserialized.expect_err("invalid source");
612            assert_eq!(error.to_string().as_str(), *result);
613        }
614    }
615
616    #[test]
617    fn deserialization_toml() {
618        let fixtures_valid = [
619            // TOML differs from YAML and JSON by always parsing integers as signed
620            (
621                r#"algorithm = "gzip"
622                   level = 8"#,
623                Compression::Gzip(CompressionLevel::Val(8)),
624            ),
625        ];
626        for (sources, result) in fixtures_valid.iter() {
627            let deserialized: Result<Compression, _> = toml::from_str(sources);
628            assert_eq!(deserialized.expect("valid source"), *result);
629        }
630    }
631
632    #[test]
633    fn from_and_to_value() {
634        let fixtures_valid = [
635            Compression::None,
636            Compression::Gzip(CompressionLevel::default()),
637            Compression::Gzip(CompressionLevel::Val(7)),
638            Compression::Zlib(CompressionLevel::Best),
639            Compression::Zlib(CompressionLevel::Val(7)),
640            Compression::Zstd(CompressionLevel::Val(6)),
641            Compression::Zstd(CompressionLevel::default()),
642            Compression::Zstd(CompressionLevel::Best),
643            Compression::Zstd(CompressionLevel::Fast),
644        ];
645
646        for v in fixtures_valid {
647            // Check serialize-deserialize round trip with defaults
648            let value = serde_json::to_value(v).unwrap();
649            serde_json::from_value::<Compression>(value).unwrap();
650        }
651    }
652}