Skip to main content

vector/sources/
logstash.rs

1use std::{
2    collections::{BTreeMap, VecDeque},
3    convert::TryFrom,
4    io,
5    net::SocketAddr,
6    num::NonZeroUsize,
7    time::Duration,
8};
9
10use bytes::{Buf, Bytes, BytesMut};
11use smallvec::{SmallVec, smallvec};
12use snafu::{ResultExt, Snafu};
13use tokio_util::codec::Decoder;
14use vector_lib::{
15    codecs::{BytesDeserializerConfig, StreamDecodingError},
16    config::{LegacyKey, LogNamespace},
17    configurable::configurable_component,
18    ipallowlist::IpAllowlistConfig,
19    lookup::{OwnedValuePath, event_path, metadata_path, owned_value_path, path},
20    schema::Definition,
21};
22use vrl::value::{KeyString, Kind, kind::Collection};
23
24use super::util::decompression::{
25    CappedDecoder, max_decompressed_size_bytes, max_zlib_compressed_frame_size_bytes,
26};
27use super::util::net::{SocketListenAddr, TcpSource, TcpSourceAck, TcpSourceAcker};
28use crate::{
29    config::{
30        DataType, GenerateConfig, Resource, SourceAcknowledgementsConfig, SourceConfig,
31        SourceContext, SourceOutput, log_schema,
32    },
33    event::{Event, LogEvent, Value},
34    serde::bool_or_struct,
35    tcp::TcpKeepaliveConfig,
36    tls::{MaybeTlsSettings, TlsSourceConfig},
37    types,
38};
39
40/// Configuration for the `logstash` source.
41#[configurable_component(source("logstash", "Collect logs from a Logstash agent."))]
42#[derive(Clone, Debug)]
43pub struct LogstashConfig {
44    #[configurable(derived)]
45    address: SocketListenAddr,
46
47    #[configurable(derived)]
48    #[configurable(metadata(docs::advanced))]
49    keepalive: Option<TcpKeepaliveConfig>,
50
51    #[configurable(derived)]
52    pub permit_origin: Option<IpAllowlistConfig>,
53
54    #[configurable(derived)]
55    tls: Option<TlsSourceConfig>,
56
57    /// The size of the receive buffer used for each connection.
58    #[configurable(metadata(docs::type_unit = "bytes"))]
59    #[configurable(metadata(docs::examples = 65536))]
60    #[configurable(metadata(docs::advanced))]
61    receive_buffer_bytes: Option<usize>,
62
63    /// The maximum number of TCP connections that are allowed at any given time.
64    #[configurable(metadata(docs::type_unit = "connections"))]
65    #[configurable(metadata(docs::advanced))]
66    connection_limit: Option<u32>,
67
68    #[configurable(derived)]
69    #[serde(default, deserialize_with = "bool_or_struct")]
70    acknowledgements: SourceAcknowledgementsConfig,
71
72    /// The namespace to use for logs. This overrides the global setting.
73    #[configurable(metadata(docs::hidden))]
74    #[serde(default)]
75    log_namespace: Option<bool>,
76}
77
78impl LogstashConfig {
79    /// Builds the `schema::Definition` for this source using the provided `LogNamespace`.
80    fn schema_definition(&self, log_namespace: LogNamespace) -> Definition {
81        // `host_key` is only inserted if not present already.
82        let host_key = log_schema()
83            .host_key()
84            .cloned()
85            .map(LegacyKey::InsertIfEmpty);
86
87        let tls_client_metadata_path = self
88            .tls
89            .as_ref()
90            .and_then(|tls| tls.client_metadata_key.as_ref())
91            .and_then(|k| k.path.clone())
92            .map(LegacyKey::Overwrite);
93
94        BytesDeserializerConfig
95            .schema_definition(log_namespace)
96            .with_standard_vector_source_metadata()
97            .with_source_metadata(
98                LogstashConfig::NAME,
99                None,
100                &owned_value_path!("timestamp"),
101                Kind::timestamp().or_undefined(),
102                Some("timestamp"),
103            )
104            .with_source_metadata(
105                LogstashConfig::NAME,
106                host_key,
107                &owned_value_path!("host"),
108                Kind::bytes(),
109                Some("host"),
110            )
111            .with_source_metadata(
112                Self::NAME,
113                tls_client_metadata_path,
114                &owned_value_path!("tls_client_metadata"),
115                Kind::object(Collection::empty().with_unknown(Kind::bytes())).or_undefined(),
116                None,
117            )
118    }
119}
120
121impl Default for LogstashConfig {
122    fn default() -> Self {
123        Self {
124            address: SocketListenAddr::SocketAddr("0.0.0.0:5044".parse().unwrap()),
125            keepalive: None,
126            permit_origin: None,
127            tls: None,
128            receive_buffer_bytes: None,
129            acknowledgements: Default::default(),
130            connection_limit: None,
131            log_namespace: None,
132        }
133    }
134}
135
136impl GenerateConfig for LogstashConfig {
137    fn generate_config() -> toml::Value {
138        toml::Value::try_from(LogstashConfig::default()).unwrap()
139    }
140}
141
142#[async_trait::async_trait]
143#[typetag::serde(name = "logstash")]
144impl SourceConfig for LogstashConfig {
145    async fn build(&self, cx: SourceContext) -> crate::Result<super::Source> {
146        let log_namespace = cx.log_namespace(self.log_namespace);
147        let source = LogstashSource {
148            timestamp_converter: types::Conversion::Timestamp(cx.globals.timezone()),
149            legacy_host_key_path: log_schema().host_key().cloned(),
150            log_namespace,
151        };
152        let shutdown_secs = Duration::from_secs(30);
153        let tls_config = self.tls.as_ref().map(|tls| tls.tls_config.clone());
154        let tls_client_metadata_key = self
155            .tls
156            .as_ref()
157            .and_then(|tls| tls.client_metadata_key.clone())
158            .and_then(|k| k.path);
159
160        let tls = MaybeTlsSettings::from_config(tls_config.as_ref(), true)?;
161        source.run(
162            self.address,
163            self.keepalive,
164            shutdown_secs,
165            tls,
166            None, // tls_reloader: not wired for this source
167            tls_client_metadata_key,
168            self.receive_buffer_bytes,
169            None,
170            cx,
171            self.acknowledgements,
172            self.connection_limit,
173            self.permit_origin.clone().map(Into::into),
174            LogstashConfig::NAME,
175            log_namespace,
176        )
177    }
178
179    fn outputs(&self, global_log_namespace: LogNamespace) -> Vec<SourceOutput> {
180        // There is a global and per-source `log_namespace` config.
181        // The source config overrides the global setting and is merged here.
182        vec![SourceOutput::new_maybe_logs(
183            DataType::Log,
184            self.schema_definition(global_log_namespace.merge(self.log_namespace)),
185        )]
186    }
187
188    fn resources(&self) -> Vec<Resource> {
189        vec![self.address.as_tcp_resource()]
190    }
191
192    fn can_acknowledge(&self) -> bool {
193        true
194    }
195}
196
197#[derive(Debug, Clone)]
198struct LogstashSource {
199    timestamp_converter: types::Conversion,
200    log_namespace: LogNamespace,
201    legacy_host_key_path: Option<OwnedValuePath>,
202}
203
204impl TcpSource for LogstashSource {
205    type Error = DecodeError;
206    type Item = LogstashEventFrame;
207    type Decoder = LogstashDecoder;
208    type Acker = LogstashAcker;
209
210    fn decoder(&self) -> Self::Decoder {
211        LogstashDecoder::new()
212    }
213
214    fn handle_events(&self, events: &mut [Event], host: SocketAddr) {
215        let now = chrono::Utc::now();
216        for event in events {
217            let log = event.as_mut_log();
218
219            self.log_namespace.insert_vector_metadata(
220                log,
221                log_schema().source_type_key(),
222                path!("source_type"),
223                Bytes::from_static(LogstashConfig::NAME.as_bytes()),
224            );
225
226            let log_timestamp = log.get(event_path!("@timestamp")).and_then(|timestamp| {
227                self.timestamp_converter
228                    .convert::<Value>(timestamp.coerce_to_bytes())
229                    .ok()
230            });
231
232            // Vector: always insert `ingest_timestamp`. Insert `timestamp` if found in event.
233            //
234            // Legacy: always insert the global log schema timestamp key- use timestamp from
235            //         event if present, otherwise use ingest.
236            match self.log_namespace {
237                LogNamespace::Vector => {
238                    if let Some(timestamp) = log_timestamp {
239                        log.insert(metadata_path!(LogstashConfig::NAME, "timestamp"), timestamp);
240                    }
241                    log.insert(metadata_path!("vector", "ingest_timestamp"), now);
242                }
243                LogNamespace::Legacy => {
244                    if let Some(timestamp_key) = log_schema().timestamp_key_target_path() {
245                        log.insert(
246                            timestamp_key,
247                            log_timestamp.unwrap_or_else(|| Value::from(now)),
248                        );
249                    }
250                }
251            }
252
253            self.log_namespace.insert_source_metadata(
254                LogstashConfig::NAME,
255                log,
256                self.legacy_host_key_path
257                    .as_ref()
258                    .map(LegacyKey::InsertIfEmpty),
259                path!("host"),
260                host.ip().to_string(),
261            );
262        }
263    }
264
265    fn build_acker(&self, frames: &[Self::Item]) -> Self::Acker {
266        LogstashAcker::new(frames)
267    }
268}
269
270struct LogstashAcker {
271    // One cumulative ACK per *completed* writer window in the batch, in wire
272    // order. We ACK only frames that complete a window (`window_end`); we never
273    // emit a partial ACK for a window that is only partially present in this
274    // batch.
275    //
276    // A partial ACK would put a sequence number on the wire that falls inside a
277    // window the client is still filling, rather than on the window boundary the
278    // client waits for. (Real clients always send exactly `window_size` events
279    // per window and wait for an ACK of that count; see the decoder's
280    // `WindowSize` handling and logstash.md.) The upstream Logstash server never
281    // emits such partial ACKs, and an intermediary (load balancer, service mesh)
282    // that buffers and later misdelivers one onto a different connection causes
283    // the client to reject it as `invalid sequence number received
284    // (seq=N, expected=M)`. A window split across batches is ACKed only once its
285    // final event arrives in a later batch. (A window can never be closed early
286    // by a new `WindowSize`: the decoder rejects a premature `WindowSize` as a
287    // fatal error, so every window the acker sees is either complete or a genuine
288    // trailing tail.)
289    acknowledgements: SmallVec<[(LogstashProtocolVersion, u32); 1]>,
290}
291
292impl LogstashAcker {
293    fn new(frames: &[LogstashEventFrame]) -> Self {
294        let acknowledgements = frames
295            .iter()
296            // ACK only completed writer windows; never a partial trailing tail.
297            .filter(|frame| frame.window_end)
298            .map(|frame| (frame.protocol, frame.sequence_number))
299            .collect();
300
301        Self { acknowledgements }
302    }
303}
304
305impl TcpSourceAcker for LogstashAcker {
306    // https://github.com/logstash-plugins/logstash-input-beats/blob/master/PROTOCOL.md#ack-frame-type
307    fn build_ack(self, ack: TcpSourceAck) -> Option<Bytes> {
308        match ack {
309            TcpSourceAck::Ack if !self.acknowledgements.is_empty() => {
310                let mut bytes: Vec<u8> = Vec::with_capacity(self.acknowledgements.len() * 6);
311                for (protocol_version, sequence_number) in self.acknowledgements {
312                    bytes.push(protocol_version.into());
313                    bytes.push(LogstashFrameType::Ack.into());
314                    bytes.extend(sequence_number.to_be_bytes().iter());
315                }
316                Some(Bytes::from(bytes))
317            }
318            _ => None,
319        }
320    }
321}
322
323#[derive(Debug)]
324enum LogstashDecoderReadState {
325    ReadProtocol,
326    ReadType(LogstashProtocolVersion),
327    ReadFrame(LogstashProtocolVersion, LogstashFrameType),
328    PendingFrames(VecDeque<(LogstashEventFrame, usize)>),
329}
330
331#[derive(Debug)]
332struct LogstashDecoder {
333    state: LogstashDecoderReadState,
334    // Tracks how many events remain in the current writer window. This lets us
335    // preserve sender window boundaries even if ReadyFrames later batches
336    // multiple decoded windows together before ACKing.
337    window_events_remaining: Option<NonZeroUsize>,
338    // Set for the decoder used to parse a decompressed payload. No known
339    // Lumberjack/Beats client emits a compressed frame nested inside another,
340    // so a nested `C` frame here is rejected rather than recursed into.
341    // Without this, an attacker could nest compressed frames arbitrarily deep
342    // and drive unbounded recursion in `decode_compressed_frame`, exhausting
343    // the stack (CWE-674).
344    nested: bool,
345}
346
347impl LogstashDecoder {
348    const fn new() -> Self {
349        Self::new_with_window_events_remaining(None)
350    }
351
352    const fn new_with_window_events_remaining(
353        window_events_remaining: Option<NonZeroUsize>,
354    ) -> Self {
355        Self {
356            state: LogstashDecoderReadState::ReadProtocol,
357            window_events_remaining,
358            nested: false,
359        }
360    }
361
362    const fn new_nested(window_events_remaining: Option<NonZeroUsize>) -> Self {
363        Self {
364            state: LogstashDecoderReadState::ReadProtocol,
365            window_events_remaining,
366            nested: true,
367        }
368    }
369
370    /// Marks whether a decoded frame closes the current writer window.
371    ///
372    /// Filebeat expects ACKs to stay within the current window announced by the
373    /// most recent `WindowSize` frame. The generic TCP batching layer can merge
374    /// frames from multiple windows before we build an ACK, so we record the
375    /// per-frame window boundary here and let the acker emit one ACK frame per
376    /// completed window later.
377    ///
378    /// If a sender omits `WindowSize`, we keep the previous behavior and treat
379    /// each standalone frame as ACKable on its own.
380    const fn annotate_frame(&mut self, frame: &mut LogstashEventFrame) {
381        match self.window_events_remaining {
382            Some(remaining) if remaining.get() == 1 => {
383                frame.window_end = true;
384                self.window_events_remaining = None;
385            }
386            Some(remaining) => {
387                frame.window_end = false;
388                self.window_events_remaining = NonZeroUsize::new(remaining.get() - 1); // safe because we know remaining is greater than 1
389            }
390            None => {
391                // Preserve existing behavior for inputs that send standalone data frames
392                // without an explicit WindowSize frame.
393                frame.window_end = true;
394            }
395        }
396    }
397}
398
399#[derive(Debug, Snafu)]
400pub enum DecodeError {
401    #[snafu(display("i/o error: {}", source))]
402    IO { source: io::Error },
403    #[snafu(display("Unknown logstash protocol version: {}", version))]
404    UnknownProtocolVersion { version: char },
405    #[snafu(display("Unknown logstash protocol message type: {}", frame_type))]
406    UnknownFrameType { frame_type: char },
407    #[snafu(display("Failed to decode JSON frame: {}", source))]
408    JsonFrameFailedDecode { source: serde_json::Error },
409    #[snafu(display("Failed to decompress compressed frame: {}", source))]
410    DecompressionFailed { source: io::Error },
411    #[snafu(display(
412        "Received a WindowSize frame before the current window completed ({remaining} events still expected)"
413    ))]
414    PrematureWindowSize { remaining: usize },
415    #[snafu(display("Compressed frame contains a nested compressed frame"))]
416    NestedCompressedFrame,
417}
418
419impl StreamDecodingError for DecodeError {
420    fn can_continue(&self) -> bool {
421        // No decode error is recoverable on this stream. Lumberjack is a
422        // length-prefixed binary protocol with no resync marker, so once a
423        // frame fails to decode the stream position is no longer trustworthy:
424        // continuing would misframe subsequent bytes and emit ACKs for bogus
425        // sequence numbers.
426        false
427    }
428}
429
430impl From<io::Error> for DecodeError {
431    fn from(source: io::Error) -> Self {
432        DecodeError::IO { source }
433    }
434}
435
436#[derive(Debug, Clone, Copy)]
437enum LogstashProtocolVersion {
438    V1, // 1
439    V2, // 2
440}
441
442impl From<LogstashProtocolVersion> for u8 {
443    fn from(frame_type: LogstashProtocolVersion) -> u8 {
444        use LogstashProtocolVersion::*;
445
446        match frame_type {
447            V1 => b'1',
448            V2 => b'2',
449        }
450    }
451}
452
453impl TryFrom<u8> for LogstashProtocolVersion {
454    type Error = DecodeError;
455
456    fn try_from(frame_type: u8) -> Result<LogstashProtocolVersion, DecodeError> {
457        use LogstashProtocolVersion::*;
458
459        match frame_type {
460            b'1' => Ok(V1),
461            b'2' => Ok(V2),
462            version => Err(DecodeError::UnknownProtocolVersion {
463                version: version as char,
464            }),
465        }
466    }
467}
468
469#[derive(Debug, Clone, Copy)]
470enum LogstashFrameType {
471    Ack,        // A
472    WindowSize, // W
473    Data,       // D
474    Json,       // J
475    Compressed, // C
476}
477
478impl From<LogstashFrameType> for u8 {
479    fn from(frame_type: LogstashFrameType) -> u8 {
480        use LogstashFrameType::*;
481
482        match frame_type {
483            Ack => b'A',
484            WindowSize => b'W',
485            Data => b'D',
486            Json => b'J',
487            Compressed => b'C',
488        }
489    }
490}
491
492impl TryFrom<u8> for LogstashFrameType {
493    type Error = DecodeError;
494
495    fn try_from(frame_type: u8) -> Result<LogstashFrameType, DecodeError> {
496        use LogstashFrameType::*;
497
498        match frame_type {
499            b'A' => Ok(Ack),
500            b'W' => Ok(WindowSize),
501            b'D' => Ok(Data),
502            b'J' => Ok(Json),
503            b'C' => Ok(Compressed),
504            frame_type => Err(DecodeError::UnknownFrameType {
505                frame_type: frame_type as char,
506            }),
507        }
508    }
509}
510
511/// Normalized event from logstash frame
512#[derive(Debug)]
513struct LogstashEventFrame {
514    protocol: LogstashProtocolVersion,
515    sequence_number: u32,
516    fields: BTreeMap<KeyString, serde_json::Value>,
517    // True when this frame completes its window (fills it to the advertised
518    // size). The acker emits one ACK per frame so marked.
519    window_end: bool,
520}
521
522struct DecodedCompressedFrames {
523    frames: VecDeque<(LogstashEventFrame, usize)>,
524    window_events_remaining: Option<NonZeroUsize>,
525}
526
527// Based on spec at: https://github.com/logstash-plugins/logstash-input-beats/blob/master/PROTOCOL.md
528// And implementation from logstash: https://github.com/logstash-plugins/logstash-input-beats/blob/27bad62a26a81fc000a9d21495b8dc7174ab63e9/src/main/java/org/logstash/beats/BeatsParser.java
529impl Decoder for LogstashDecoder {
530    type Item = (LogstashEventFrame, usize);
531    type Error = DecodeError;
532
533    fn decode(&mut self, src: &mut BytesMut) -> Result<Option<Self::Item>, Self::Error> {
534        // This implements a sort of simple state machine to read the frames from the wire
535        //
536        // Each matched arm with either:
537        // * Return that there is not enough data
538        // * Return an error
539        // * Read some bytes and advance the state
540        loop {
541            self.state = match self.state {
542                // if we have any unsent frames, send them before reading new logstash frame
543                LogstashDecoderReadState::PendingFrames(ref mut frames) => {
544                    match frames.pop_front() {
545                        Some(frame) => return Ok(Some(frame)),
546                        None => LogstashDecoderReadState::ReadProtocol,
547                    }
548                }
549                LogstashDecoderReadState::ReadProtocol => {
550                    if src.remaining() < 1 {
551                        return Ok(None);
552                    }
553
554                    use LogstashProtocolVersion::*;
555
556                    match LogstashProtocolVersion::try_from(src.get_u8())? {
557                        V1 => LogstashDecoderReadState::ReadType(V1),
558                        V2 => LogstashDecoderReadState::ReadType(V2),
559                    }
560                }
561                LogstashDecoderReadState::ReadType(protocol) => {
562                    if src.remaining() < 1 {
563                        return Ok(None);
564                    }
565
566                    use LogstashFrameType::*;
567
568                    match LogstashFrameType::try_from(src.get_u8())? {
569                        WindowSize => LogstashDecoderReadState::ReadFrame(protocol, WindowSize),
570                        Data => LogstashDecoderReadState::ReadFrame(protocol, Data),
571                        Json => LogstashDecoderReadState::ReadFrame(protocol, Json),
572                        Compressed => LogstashDecoderReadState::ReadFrame(protocol, Compressed),
573                        Ack => LogstashDecoderReadState::ReadFrame(protocol, Ack),
574                    }
575                }
576                // The window size tells us how many events the writer will send
577                // in this window before waiting for an ACK. We count those events
578                // down (in `annotate_frame`) so the acker can mark the window's
579                // boundary, even when ReadyFrames batches several windows together.
580                //
581                // The protocol spec defines window size as a *maximum* unacked
582                // count, which read literally would let a writer underfill a
583                // window and then open a new one. In practice every real client
584                // (go-lumber, beats) sets the window size to the exact number of
585                // events it then sends, and the reference go-lumber server treats
586                // it as exact: its `readEvents` loop reads exactly `window_size`
587                // frames and accepts only `J`/`D`/`C` frame bytes inside that loop,
588                // so any other byte — including a premature `W` — hits the
589                // `default` branch and returns `ErrProtocolError`, closing the
590                // connection (`go-lumber/server/v2/reader.go:121-124`, v1:
591                // `go-lumber/server/v1/reader.go:117-120`). We rely on that
592                // observed behavior, not the looser spec wording. See the
593                // "Window size" section of logstash.md for the client/server
594                // references.
595                //
596                // https://github.com/logstash-plugins/logstash-input-beats/blob/master/PROTOCOL.md#window-size-frame-type
597                LogstashDecoderReadState::ReadFrame(_protocol, LogstashFrameType::WindowSize) => {
598                    // A new window must not open until the current one has received
599                    // all advertised events. Because real clients always fill a
600                    // window exactly (see above), a `WindowSize` arriving mid-window
601                    // means the sender has desynced from the window contract; reject
602                    // it as a fatal decode error (matching the reference server)
603                    // rather than guess an ACK boundary for the abandoned window.
604                    // Only a `WindowSize` can open a window, so it is the only frame
605                    // that can prematurely close one. This guarantees every window
606                    // the acker sees is either complete or a genuine trailing tail.
607                    if let Some(remaining) = self.window_events_remaining {
608                        return Err(DecodeError::PrematureWindowSize {
609                            remaining: remaining.get(),
610                        });
611                    }
612
613                    if src.remaining() < 4 {
614                        return Ok(None);
615                    }
616
617                    let window_size = src.get_u32() as usize;
618                    self.window_events_remaining = NonZeroUsize::new(window_size);
619
620                    LogstashDecoderReadState::ReadProtocol
621                }
622                // we shouldn't receive acks from the writer, just skip
623                //
624                // https://github.com/logstash-plugins/logstash-input-beats/blob/master/PROTOCOL.md#ack-frame-type
625                LogstashDecoderReadState::ReadFrame(_protocol, LogstashFrameType::Ack) => {
626                    if src.remaining() < 4 {
627                        return Ok(None);
628                    }
629
630                    let _sequence_number = src.get_u32();
631
632                    LogstashDecoderReadState::ReadProtocol
633                }
634                // https://github.com/logstash-plugins/logstash-input-beats/blob/master/PROTOCOL.md#data-frame-type
635                LogstashDecoderReadState::ReadFrame(protocol, LogstashFrameType::Data) => {
636                    let Some((mut frame, byte_size)) = decode_data_frame(protocol, src) else {
637                        return Ok(None);
638                    };
639                    self.annotate_frame(&mut frame);
640
641                    LogstashDecoderReadState::PendingFrames([(frame, byte_size)].into())
642                }
643                // https://github.com/logstash-plugins/logstash-input-beats/blob/master/PROTOCOL.md#json-frame-type
644                LogstashDecoderReadState::ReadFrame(protocol, LogstashFrameType::Json) => {
645                    let Some((mut frame, byte_size)) = decode_json_frame(protocol, src)? else {
646                        return Ok(None);
647                    };
648                    self.annotate_frame(&mut frame);
649
650                    LogstashDecoderReadState::PendingFrames([(frame, byte_size)].into())
651                }
652                // https://github.com/logstash-plugins/logstash-input-beats/blob/master/PROTOCOL.md#compressed-frame-type
653                //
654                // The compressed payload is still part of the same logical Lumberjack stream, so
655                // the nested decoder must inherit the current window state and return the updated
656                // state after expanding the payload. Re-annotating the emitted frames here would
657                // overwrite any WindowSize boundaries that were established inside the compressed
658                // payload and can also lose progress from a partially consumed outer window.
659                LogstashDecoderReadState::ReadFrame(_protocol, LogstashFrameType::Compressed) => {
660                    if self.nested {
661                        return Err(DecodeError::NestedCompressedFrame);
662                    }
663
664                    let Some(decoded) = decode_compressed_frame(src, self.window_events_remaining)?
665                    else {
666                        return Ok(None);
667                    };
668                    self.window_events_remaining = decoded.window_events_remaining;
669
670                    LogstashDecoderReadState::PendingFrames(decoded.frames)
671                }
672            };
673        }
674    }
675}
676
677/// Decode the Lumberjack version 1 protocol, which use the Key:Value format.
678fn decode_data_frame(
679    protocol: LogstashProtocolVersion,
680    src: &mut BytesMut,
681) -> Option<(LogstashEventFrame, usize)> {
682    let mut rest = src.as_ref();
683
684    if rest.remaining() < 8 {
685        return None;
686    }
687    let sequence_number = rest.get_u32();
688    let pair_count = rest.get_u32();
689    if pair_count == 0 {
690        return None; // Invalid number of fields
691    }
692
693    let mut fields = BTreeMap::<KeyString, serde_json::Value>::new();
694    for _ in 0..pair_count {
695        let (key, value, right) = decode_pair(rest)?;
696        rest = right;
697
698        fields.insert(
699            String::from_utf8_lossy(key).into(),
700            String::from_utf8_lossy(value).into(),
701        );
702    }
703
704    let byte_size = bytes_remaining(src, rest);
705    src.advance(byte_size);
706
707    Some((
708        LogstashEventFrame {
709            protocol,
710            sequence_number,
711            fields,
712            window_end: false,
713        },
714        byte_size,
715    ))
716}
717
718fn decode_pair(mut rest: &[u8]) -> Option<(&[u8], &[u8], &[u8])> {
719    if rest.remaining() < 4 {
720        return None;
721    }
722    let key_length = rest.get_u32() as usize;
723
724    if rest.remaining() < key_length {
725        return None;
726    }
727    let (key, right) = rest.split_at(key_length);
728    rest = right;
729
730    if rest.remaining() < 4 {
731        return None;
732    }
733    let value_length = rest.get_u32() as usize;
734    if rest.remaining() < value_length {
735        return None;
736    }
737    let (value, right) = rest.split_at(value_length);
738    Some((key, value, right))
739}
740
741fn decode_json_frame(
742    protocol: LogstashProtocolVersion,
743    src: &mut BytesMut,
744) -> Result<Option<(LogstashEventFrame, usize)>, DecodeError> {
745    let mut rest = src.as_ref();
746
747    if rest.remaining() < 8 {
748        return Ok(None);
749    }
750    let sequence_number = rest.get_u32();
751    let payload_size = rest.get_u32() as usize;
752
753    if rest.remaining() < payload_size {
754        return Ok(None);
755    }
756
757    let (slice, right) = rest.split_at(payload_size);
758    rest = right;
759
760    let fields: BTreeMap<KeyString, serde_json::Value> =
761        serde_json::from_slice(slice).context(JsonFrameFailedDecodeSnafu {})?;
762
763    let byte_size = bytes_remaining(src, rest);
764    src.advance(byte_size);
765
766    Ok(Some((
767        LogstashEventFrame {
768            protocol,
769            sequence_number,
770            fields,
771            window_end: false,
772        },
773        byte_size,
774    )))
775}
776
777fn decode_compressed_frame(
778    src: &mut BytesMut,
779    window_events_remaining: Option<NonZeroUsize>,
780) -> Result<Option<DecodedCompressedFrames>, DecodeError> {
781    let mut rest = src.as_ref();
782
783    if rest.remaining() < 4 {
784        return Ok(None);
785    }
786    let payload_size = rest.get_u32() as usize;
787    let limit = max_decompressed_size_bytes();
788
789    // Reject an oversized declared payload before buffering it, so a peer cannot force multi-GB
790    // buffering by advertising a huge length and slow-streaming its bytes. The bound includes
791    // zlib's worst-case expansion so a valid frame whose decompressed content is within `limit`
792    // is never rejected here; the decompressed cap itself is still enforced below.
793    let compressed_limit = max_zlib_compressed_frame_size_bytes();
794    if payload_size > compressed_limit {
795        return Err(DecodeError::DecompressionFailed {
796            source: io::Error::other(format!(
797                "compressed frame payload size {payload_size} exceeds limit of {compressed_limit} bytes"
798            )),
799        });
800    }
801
802    if rest.remaining() < payload_size {
803        return Ok(None);
804    }
805
806    let (slice, right) = rest.split_at(payload_size);
807    rest = right;
808
809    let res = CappedDecoder::zlib_with_limit(io::Cursor::new(slice), limit)
810        .decompress()
811        .map(|v| BytesMut::from(v.as_slice()))
812        .context(DecompressionFailedSnafu);
813
814    let byte_size = bytes_remaining(src, rest);
815    src.advance(byte_size);
816
817    let mut buf = res?;
818
819    let mut decoder = LogstashDecoder::new_nested(window_events_remaining);
820
821    let mut frames = VecDeque::new();
822
823    while let Some(s) = decoder.decode(&mut buf)? {
824        frames.push_back(s);
825    }
826    Ok(Some(DecodedCompressedFrames {
827        frames,
828        window_events_remaining: decoder.window_events_remaining,
829    }))
830}
831
832fn bytes_remaining(src: &BytesMut, rest: &[u8]) -> usize {
833    let remaining = rest.remaining();
834    src.remaining() - remaining
835}
836
837impl From<LogstashEventFrame> for Event {
838    fn from(frame: LogstashEventFrame) -> Self {
839        Event::Log(LogEvent::from(
840            frame
841                .fields
842                .into_iter()
843                .map(|(key, value)| (key, Value::from(value)))
844                .collect::<BTreeMap<_, _>>(),
845        ))
846    }
847}
848
849impl From<LogstashEventFrame> for SmallVec<[Event; 1]> {
850    fn from(frame: LogstashEventFrame) -> Self {
851        smallvec![frame.into()]
852    }
853}
854
855#[cfg(test)]
856mod test {
857    use std::io::Write;
858
859    use bytes::BufMut;
860    use flate2::{Compression, write::ZlibEncoder};
861    use futures::{Stream, StreamExt, stream};
862    use rand::{Rng, rng};
863    use tokio::io::{AsyncReadExt, AsyncWriteExt};
864    use vector_lib::codecs::ReadyFrames;
865    use vector_lib::lookup::OwnedTargetPath;
866    use vrl::event_path;
867    use vrl::value::kind::Collection;
868
869    use super::*;
870    use crate::{
871        SourceSender,
872        event::EventStatus,
873        test_util::{
874            addr::next_addr,
875            components::{SOCKET_PUSH_SOURCE_TAGS, assert_source_compliance},
876            spawn_collect_n, wait_for_tcp,
877        },
878    };
879
880    #[test]
881    fn generate_config() {
882        crate::test_util::test_generate_config::<LogstashConfig>();
883    }
884
885    #[tokio::test]
886    async fn test_delivered() {
887        test_protocol(EventStatus::Delivered, true).await;
888    }
889
890    #[tokio::test]
891    async fn test_failed() {
892        test_protocol(EventStatus::Rejected, false).await;
893    }
894
895    async fn start_logstash(
896        status: EventStatus,
897    ) -> (SocketAddr, impl Stream<Item = Event> + Unpin) {
898        let (sender, recv) = SourceSender::new_test_finalize(status);
899        let (_guard, address) = next_addr();
900        let source = LogstashConfig {
901            address: address.into(),
902            tls: None,
903            permit_origin: None,
904            keepalive: None,
905            receive_buffer_bytes: None,
906            acknowledgements: true.into(),
907            connection_limit: None,
908            log_namespace: None,
909        }
910        .build(SourceContext::new_test(sender, None))
911        .await
912        .unwrap();
913        tokio::spawn(source);
914        wait_for_tcp(address).await;
915        (address, recv)
916    }
917
918    async fn test_protocol(status: EventStatus, sends_ack: bool) {
919        let events = assert_source_compliance(&SOCKET_PUSH_SOURCE_TAGS, async {
920            let (address, recv) = start_logstash(status).await;
921            spawn_collect_n(
922                send_req(address, &[("message", "Hello, world!")], sends_ack),
923                recv,
924                1,
925            )
926            .await
927        })
928        .await;
929
930        assert_eq!(events.len(), 1);
931        let log = events[0].as_log();
932        assert_eq!(
933            log.get(event_path!("message")).unwrap().to_string_lossy(),
934            "Hello, world!".to_string()
935        );
936        assert_eq!(
937            log.get(event_path!("source_type"))
938                .unwrap()
939                .to_string_lossy(),
940            "logstash".to_string()
941        );
942        assert!(log.get(event_path!("host")).is_some());
943        assert!(log.get(event_path!("timestamp")).is_some());
944    }
945
946    fn push_req(req: &mut BytesMut, seq: u32, pairs: &[(&str, &str)]) {
947        req.put_u8(b'2');
948        req.put_u8(b'D');
949        req.put_u32(seq);
950        req.put_u32(pairs.len() as u32);
951        for (key, value) in pairs {
952            req.put_u32(key.len() as u32);
953            req.put(key.as_bytes());
954            req.put_u32(value.len() as u32);
955            req.put(value.as_bytes());
956        }
957    }
958
959    fn encode_req(seq: u32, pairs: &[(&str, &str)]) -> Bytes {
960        let mut req = BytesMut::new();
961        push_req(&mut req, seq, pairs);
962        req.into()
963    }
964
965    fn push_window_size(req: &mut BytesMut, size: u32) {
966        req.put_u8(b'2');
967        req.put_u8(b'W');
968        req.put_u32(size);
969    }
970
971    fn push_compressed(req: &mut BytesMut, inner: &[u8]) {
972        let mut encoder = ZlibEncoder::new(Vec::new(), Compression::default());
973        encoder.write_all(inner).unwrap();
974        let compressed = encoder.finish().unwrap();
975
976        req.put_u8(b'2');
977        req.put_u8(b'C');
978        req.put_u32(compressed.len() as u32);
979        req.put(compressed.as_slice());
980    }
981
982    fn decode_frames(mut src: BytesMut) -> Vec<(LogstashEventFrame, usize)> {
983        let mut decoder = LogstashDecoder::new();
984        let mut frames = Vec::new();
985
986        while let Some(frame) = decoder.decode(&mut src).unwrap() {
987            frames.push(frame);
988        }
989
990        assert_eq!(src.len(), 0);
991        frames
992    }
993
994    fn decode_acknowledgements(mut ack: Bytes) -> Vec<u32> {
995        let mut acknowledgements = Vec::new();
996
997        while !ack.is_empty() {
998            assert!(
999                ack.len() >= 6,
1000                "ack stream ended with {} trailing bytes",
1001                ack.len()
1002            );
1003            assert_eq!(ack.get_u8(), b'2');
1004            assert_eq!(ack.get_u8(), b'A');
1005            acknowledgements.push(ack.get_u32());
1006        }
1007
1008        acknowledgements
1009    }
1010
1011    fn decoded_sequence_numbers(decoded: &[(LogstashEventFrame, usize)]) -> Vec<u32> {
1012        decoded
1013            .iter()
1014            .map(|(frame, _)| frame.sequence_number)
1015            .collect::<Vec<_>>()
1016    }
1017
1018    fn assert_decoded_sequences(
1019        decoded: &[(LogstashEventFrame, usize)],
1020        expected_sequences: &[u32],
1021    ) {
1022        assert_eq!(decoded_sequence_numbers(decoded), expected_sequences);
1023    }
1024
1025    async fn assert_acknowledgements_for_ready_frames(
1026        decoded: Vec<(LogstashEventFrame, usize)>,
1027        expected_sequences: &[u32],
1028        expected_acknowledgements: &[u32],
1029    ) {
1030        assert_decoded_sequences(&decoded, expected_sequences);
1031
1032        let stream = stream::iter(decoded.into_iter().map(Ok::<_, DecodeError>));
1033        let mut ready = ReadyFrames::with_capacity(stream, 16);
1034        let (frames, _) = ready.next().await.unwrap().unwrap();
1035
1036        // An incomplete window produces no ACK at all (`build_ack` returns
1037        // `None`); treat that as an empty acknowledgement list so callers can
1038        // assert it with `&[]`.
1039        let acknowledgements = LogstashAcker::new(&frames)
1040            .build_ack(TcpSourceAck::Ack)
1041            .map_or_else(Vec::new, decode_acknowledgements);
1042
1043        assert!(ready.next().await.is_none());
1044        assert_eq!(acknowledgements, expected_acknowledgements);
1045    }
1046
1047    fn decode_frames_and_assert_sequences(
1048        src: BytesMut,
1049        expected_sequences: &[u32],
1050    ) -> Vec<(LogstashEventFrame, usize)> {
1051        let decoded = decode_frames(src);
1052        assert_decoded_sequences(&decoded, expected_sequences);
1053        decoded
1054    }
1055
1056    fn decode_frames_with_decoder(
1057        decoder: &mut LogstashDecoder,
1058        mut src: BytesMut,
1059    ) -> Vec<(LogstashEventFrame, usize)> {
1060        let mut frames = Vec::new();
1061
1062        while let Some(frame) = decoder.decode(&mut src).unwrap() {
1063            frames.push(frame);
1064        }
1065
1066        assert_eq!(src.len(), 0);
1067        frames
1068    }
1069
1070    fn decode_frames_with_decoder_and_assert_sequences(
1071        decoder: &mut LogstashDecoder,
1072        src: BytesMut,
1073        expected_sequences: &[u32],
1074    ) -> Vec<(LogstashEventFrame, usize)> {
1075        let decoded = decode_frames_with_decoder(decoder, src);
1076        assert_decoded_sequences(&decoded, expected_sequences);
1077        decoded
1078    }
1079
1080    #[test]
1081    fn v1_decoder_does_not_panic() {
1082        let seq = rng().random_range(1..u32::MAX);
1083        let req = encode_req(seq, &[("message", "Hello, World!")]);
1084        for i in 0..req.len() - 1 {
1085            assert!(
1086                decode_data_frame(LogstashProtocolVersion::V1, &mut BytesMut::from(&req[..i]))
1087                    .is_none()
1088            );
1089        }
1090    }
1091
1092    // A malformed frame must be a fatal (non-continuable) decode error: the
1093    // Lumberjack stream can't be resynced, so the connection is closed rather
1094    // than continuing with a desynced decoder (which would emit bogus ACKs).
1095    // This matches upstream logstash-input-beats, which closes the channel on
1096    // any decode exception.
1097
1098    #[test]
1099    fn malformed_json_frame_is_a_fatal_decode_error() {
1100        let mut decoder = LogstashDecoder::new();
1101        let mut src = BytesMut::new();
1102        src.put_u8(b'2');
1103        src.put_u8(b'J');
1104        src.put_u32(1); // sequence number
1105        let bad = b"{ not valid json ";
1106        src.put_u32(bad.len() as u32); // payload size
1107        src.put(&bad[..]);
1108
1109        let err = decoder.decode(&mut src).unwrap_err();
1110        assert!(matches!(err, DecodeError::JsonFrameFailedDecode { .. }));
1111        assert!(
1112            !err.can_continue(),
1113            "a malformed JSON frame must be fatal so the connection closes",
1114        );
1115    }
1116
1117    #[test]
1118    fn malformed_compressed_frame_is_a_fatal_decode_error() {
1119        let mut decoder = LogstashDecoder::new();
1120        let mut src = BytesMut::new();
1121        src.put_u8(b'2');
1122        src.put_u8(b'C');
1123        let garbage = b"this is not a zlib stream";
1124        src.put_u32(garbage.len() as u32); // payload size
1125        src.put(&garbage[..]);
1126
1127        let err = decoder.decode(&mut src).unwrap_err();
1128        assert!(matches!(err, DecodeError::DecompressionFailed { .. }));
1129        assert!(!err.can_continue());
1130    }
1131
1132    #[test]
1133    fn premature_window_size_frame_is_a_fatal_decode_error() {
1134        // A WindowSize frame that arrives before the current window has received
1135        // all its advertised events is a protocol violation, matching the
1136        // upstream Logstash/go-lumber server. The reference server's `readEvents`
1137        // loop reads exactly `window_size` frames and only accepts `J`/`D`/`C`
1138        // frame bytes inside that loop; any other byte — including a premature
1139        // `W` — hits its `default` branch and returns `ErrProtocolError`, closing
1140        // the connection (`go-lumber/server/v2/reader.go:121-124`). Every real
1141        // client (go-lumber, beats) sends exactly `window_size` events per window,
1142        // so a mid-window `WindowSize` means the sender has desynced; see the
1143        // "Window size" section of logstash.md. Rejecting it here means an
1144        // under-filled window can never reach the acker, so the acker never has to
1145        // invent a boundary for a window the sender closed early.
1146        let mut decoder = LogstashDecoder::new();
1147        let mut src = BytesMut::new();
1148        push_window_size(&mut src, 2);
1149        push_req(&mut src, 1, &[("message", "only one of two")]);
1150        push_window_size(&mut src, 5); // premature: window 1 still expects another event
1151
1152        // The first decode yields the single data frame of the incomplete window.
1153        assert!(decoder.decode(&mut src).unwrap().is_some());
1154
1155        // The next decode reaches the premature WindowSize and fails fatally so
1156        // the connection is closed rather than silently re-framed.
1157        let err = decoder.decode(&mut src).unwrap_err();
1158        assert!(matches!(err, DecodeError::PrematureWindowSize { .. }));
1159        assert!(
1160            !err.can_continue(),
1161            "a premature WindowSize must be fatal so the connection closes",
1162        );
1163    }
1164
1165    #[test]
1166    fn premature_window_size_inside_compressed_payload_is_fatal() {
1167        // The premature-WindowSize guard also fires inside a compressed payload:
1168        // the nested decoder run by `decode_compressed_frame` hits the guard, and
1169        // the error propagates out through the `?` on the nested decode loop so
1170        // the outer decode returns it. This locks in that error propagation.
1171        let mut inner = BytesMut::new();
1172        push_window_size(&mut inner, 2);
1173        push_req(&mut inner, 1, &[("message", "only one of two")]);
1174        push_window_size(&mut inner, 5); // premature, inside the compressed payload
1175
1176        let mut req = BytesMut::new();
1177        push_compressed(&mut req, &inner);
1178
1179        let mut decoder = LogstashDecoder::new();
1180        let err = decoder.decode(&mut req).unwrap_err();
1181        assert!(matches!(err, DecodeError::PrematureWindowSize { .. }));
1182        assert!(
1183            !err.can_continue(),
1184            "a premature WindowSize inside a compressed frame must be fatal",
1185        );
1186    }
1187
1188    #[test]
1189    fn nested_compressed_frame_is_a_fatal_decode_error() {
1190        let mut inner = BytesMut::new();
1191        push_req(&mut inner, 1, &[("message", "should never be reached")]);
1192
1193        let mut middle = BytesMut::new();
1194        push_compressed(&mut middle, &inner);
1195
1196        let mut req = BytesMut::new();
1197        push_compressed(&mut req, &middle);
1198
1199        let mut decoder = LogstashDecoder::new();
1200        let err = decoder.decode(&mut req).unwrap_err();
1201        assert!(matches!(err, DecodeError::NestedCompressedFrame));
1202        assert!(!err.can_continue());
1203    }
1204
1205    #[tokio::test]
1206    async fn malformed_frame_closes_connection_without_ack() {
1207        let (address, _recv) = start_logstash(EventStatus::Delivered).await;
1208
1209        let mut socket = tokio::net::TcpStream::connect(address).await.unwrap();
1210
1211        // A '2' 'J' frame whose payload is not valid JSON.
1212        let mut req = BytesMut::new();
1213        req.put_u8(b'2');
1214        req.put_u8(b'J');
1215        req.put_u32(1); // sequence number
1216        let bad = b"{ not valid json ";
1217        req.put_u32(bad.len() as u32); // payload size
1218        req.put(&bad[..]);
1219        socket.write_all(&req).await.unwrap();
1220
1221        // The source must close the connection on the decode error and send no
1222        // ACK; the client will reconnect and retransmit.
1223        let mut output = BytesMut::new();
1224        let result = socket.read_buf(&mut output).await;
1225        assert!(
1226            matches!(result, Ok(0)) || result.is_err(),
1227            "expected the connection to close; read returned {result:?} with {output:?}",
1228        );
1229        assert!(
1230            output.is_empty(),
1231            "no ACK should be sent for a malformed frame, got {output:?}",
1232        );
1233    }
1234
1235    #[tokio::test]
1236    async fn distinct_windows_do_not_share_an_ack_domain() {
1237        let mut req = BytesMut::new();
1238        push_window_size(&mut req, 1);
1239        push_req(&mut req, 1, &[("message", "first window")]);
1240        push_window_size(&mut req, 2);
1241        push_req(&mut req, 1, &[("message", "second window first")]);
1242        push_req(&mut req, 2, &[("message", "second window second")]);
1243
1244        let decoded = decode_frames_and_assert_sequences(req, &[1, 1, 2]);
1245        assert_acknowledgements_for_ready_frames(decoded, &[1, 1, 2], &[1, 2]).await;
1246    }
1247
1248    #[tokio::test]
1249    async fn distinct_windows_with_monotonic_sequences_ack_the_first_window() {
1250        let mut req = BytesMut::new();
1251        push_window_size(&mut req, 2);
1252        push_req(&mut req, 1, &[("message", "first window first")]);
1253        push_req(&mut req, 2, &[("message", "first window second")]);
1254        push_window_size(&mut req, 2);
1255        push_req(&mut req, 3, &[("message", "second window first")]);
1256        push_req(&mut req, 4, &[("message", "second window second")]);
1257
1258        let decoded = decode_frames_and_assert_sequences(req, &[1, 2, 3, 4]);
1259        assert_acknowledgements_for_ready_frames(decoded, &[1, 2, 3, 4], &[2, 4]).await;
1260    }
1261
1262    #[tokio::test]
1263    async fn incomplete_window_is_not_acked() {
1264        // A window that has not yet received all its events must not be ACKed.
1265        // Emitting a partial ACK here puts a sequence number on the wire that
1266        // does not correspond to any window boundary the client declared; if
1267        // that ACK is later misattributed to a different (smaller) window by an
1268        // intermediary, the client rejects it as `invalid sequence number
1269        // received`. Matching the upstream Logstash server, we only ACK once
1270        // the window's final event arrives.
1271        let mut req = BytesMut::new();
1272        push_window_size(&mut req, 4);
1273        push_req(&mut req, 1, &[("message", "only event in partial window")]);
1274
1275        let decoded = decode_frames_and_assert_sequences(req, &[1]);
1276        assert_acknowledgements_for_ready_frames(decoded, &[1], &[]).await;
1277    }
1278
1279    #[tokio::test]
1280    async fn window_split_across_compressed_frames_acks_once_on_completion() {
1281        // A single window whose advertised size exceeds the number of events in
1282        // any one compressed frame is split across several compressed frames. The
1283        // decoder must thread `window_events_remaining` out of each
1284        // `decode_compressed_frame` so the countdown continues across the
1285        // compression boundary: only the window's true final event is marked
1286        // `window_end`, yielding exactly one ACK. A threading bug would either
1287        // mark a spurious boundary inside an earlier compressed frame (two
1288        // window_ends -> two ACKs) or never complete the window at all.
1289        let mut first = BytesMut::new();
1290        push_req(&mut first, 1, &[("message", "w4 first")]);
1291        push_req(&mut first, 2, &[("message", "w4 second")]);
1292
1293        let mut second = BytesMut::new();
1294        push_req(&mut second, 3, &[("message", "w4 third")]);
1295        push_req(&mut second, 4, &[("message", "w4 fourth")]);
1296
1297        // WindowSize(4) is sent uncompressed (as beats does), then the four
1298        // events arrive two-per-compressed-frame, so each compressed frame
1299        // carries fewer events than the window size.
1300        let mut req = BytesMut::new();
1301        push_window_size(&mut req, 4);
1302        push_compressed(&mut req, &first);
1303        push_compressed(&mut req, &second);
1304
1305        let decoded = decode_frames_and_assert_sequences(req, &[1, 2, 3, 4]);
1306        assert_acknowledgements_for_ready_frames(decoded, &[1, 2, 3, 4], &[4]).await;
1307    }
1308
1309    #[tokio::test]
1310    async fn window_larger_than_ready_frames_capacity_in_one_compressed_frame_acks_once() {
1311        const WINDOW: u32 = 5;
1312        const CAPACITY: usize = 2;
1313
1314        let mut inner = BytesMut::new();
1315        for seq in 1..=WINDOW {
1316            push_req(&mut inner, seq, &[("message", "event in oversized window")]);
1317        }
1318
1319        // A following small window. Its sequence numbers restart at 1 (per the
1320        // protocol), and crucially its first event will share a ReadyFrames batch
1321        // with the oversized window's completing event (seq 5). That batch must
1322        // ACK only the oversized window (seq 5); the small window's retained first
1323        // event must NOT produce a second ACK until the small window's own final
1324        // event arrives.
1325        const SMALL_WINDOW: u32 = 2;
1326        let mut small_inner = BytesMut::new();
1327        for seq in 1..=SMALL_WINDOW {
1328            push_req(
1329                &mut small_inner,
1330                seq,
1331                &[("message", "event in small window")],
1332            );
1333        }
1334
1335        // WindowSize is sent uncompressed (as beats does), then each whole window
1336        // arrives in a single compressed frame, exactly as in the report.
1337        let mut req = BytesMut::new();
1338        push_window_size(&mut req, WINDOW);
1339        push_compressed(&mut req, &inner);
1340        push_window_size(&mut req, SMALL_WINDOW);
1341        push_compressed(&mut req, &small_inner);
1342
1343        let decoded = decode_frames_and_assert_sequences(req, &[1, 2, 3, 4, 5, 1, 2]);
1344
1345        let stream = stream::iter(decoded.into_iter().map(Ok::<_, DecodeError>));
1346        let mut ready = ReadyFrames::with_capacity(stream, CAPACITY);
1347        let mut acknowledgements = Vec::new();
1348
1349        while let Some(result) = ready.next().await {
1350            let (frames, _byte_size) = result.unwrap();
1351            let acks = LogstashAcker::new(&frames)
1352                .build_ack(TcpSourceAck::Ack)
1353                .map_or_else(Vec::new, decode_acknowledgements);
1354            acknowledgements.push(acks);
1355        }
1356
1357        // Batches: [1, 2] [3, 4] [5, 1] [2].
1358        // - The oversized window completes in the [5, 1] batch -> ACK(5) only; the
1359        //   retained small-window seq 1 in that same batch is NOT a window end, so
1360        //   it produces no ACK.
1361        // - The small window completes in the [2] batch -> ACK(2).
1362        // Each window is therefore ACKed exactly once; the oversized window's
1363        // sequence number (5) never reappears in a later batch.
1364        assert_eq!(acknowledgements, vec![vec![], vec![], vec![5], vec![2]]);
1365
1366        // Each window is ACKed exactly once: seq 5 (oversized) and seq 2 (small).
1367        let all_acks: Vec<u32> = acknowledgements.into_iter().flatten().collect();
1368        assert_eq!(
1369            all_acks,
1370            vec![5, 2],
1371            "each window must be ACKed exactly once"
1372        );
1373    }
1374
1375    #[tokio::test]
1376    async fn complete_window_then_incomplete_window_acks_only_the_complete_one() {
1377        // The customer-reported failure: a small complete window followed by a
1378        // larger window that is only partially present in the same batch. The
1379        // acker must emit only the completed window's ACK and must not emit a
1380        // partial ACK for the incomplete trailing window, whose sequence number
1381        // would otherwise exceed the smaller window the client awaits.
1382        let mut req = BytesMut::new();
1383        push_window_size(&mut req, 2);
1384        push_req(&mut req, 1, &[("message", "complete window first")]);
1385        push_req(&mut req, 2, &[("message", "complete window second")]);
1386        push_window_size(&mut req, 1000);
1387        push_req(&mut req, 1, &[("message", "partial window first")]);
1388        push_req(&mut req, 2, &[("message", "partial window second")]);
1389        push_req(&mut req, 3, &[("message", "partial window third")]);
1390
1391        let decoded = decode_frames_and_assert_sequences(req, &[1, 2, 1, 2, 3]);
1392        assert_acknowledgements_for_ready_frames(decoded, &[1, 2, 1, 2, 3], &[2]).await;
1393    }
1394
1395    #[tokio::test]
1396    async fn compressed_frames_preserve_inner_window_boundaries() {
1397        let mut inner = BytesMut::new();
1398        push_window_size(&mut inner, 2);
1399        push_req(&mut inner, 1, &[("message", "compressed first")]);
1400        push_req(&mut inner, 2, &[("message", "compressed second")]);
1401
1402        let mut req = BytesMut::new();
1403        push_compressed(&mut req, &inner);
1404
1405        let decoded = decode_frames_and_assert_sequences(req, &[1, 2]);
1406        assert_acknowledgements_for_ready_frames(decoded, &[1, 2], &[2]).await;
1407    }
1408
1409    #[tokio::test]
1410    async fn single_window_split_across_ready_frames_acks_only_on_completion() {
1411        // When one writer window is split across multiple `ReadyFrames` batches,
1412        // only the batch containing the window's final event (its `window_end`)
1413        // produces an ACK. Earlier batches hold no window boundary, so they emit
1414        // nothing rather than a partial ACK for a mid-window sequence number.
1415        let mut req = BytesMut::new();
1416        push_window_size(&mut req, 4);
1417        push_req(&mut req, 1, &[("message", "first")]);
1418        push_req(&mut req, 2, &[("message", "second")]);
1419        push_req(&mut req, 3, &[("message", "third")]);
1420        push_req(&mut req, 4, &[("message", "fourth")]);
1421
1422        let decoded = decode_frames_and_assert_sequences(req, &[1, 2, 3, 4]);
1423
1424        let stream = stream::iter(decoded.into_iter().map(Ok::<_, DecodeError>));
1425        let mut ready = ReadyFrames::with_capacity(stream, 2);
1426        let mut acknowledgements = Vec::new();
1427
1428        while let Some(result) = ready.next().await {
1429            let (frames, _byte_size) = result.unwrap();
1430            let acks = LogstashAcker::new(&frames)
1431                .build_ack(TcpSourceAck::Ack)
1432                .map_or_else(Vec::new, decode_acknowledgements);
1433            acknowledgements.push(acks);
1434        }
1435
1436        // First batch (seq 1, 2) holds no window boundary -> no ACK.
1437        // Second batch (seq 3, 4) completes the window -> ACK(4).
1438        assert_eq!(acknowledgements, vec![vec![], vec![4]]);
1439    }
1440
1441    #[tokio::test]
1442    async fn fresh_window_after_completed_window_is_accepted() {
1443        // A decoder reused across reads accepts a fresh window once the previous
1444        // window has completed. This is the legitimate counterpart to the
1445        // premature-WindowSize error: a `WindowSize` is only rejected when it
1446        // arrives mid-window, so opening a new window after the prior one has
1447        // received all its advertised events is always allowed. (A `WindowSize`
1448        // after a still-partial window would now be a fatal protocol error; see
1449        // `premature_window_size_frame_is_a_fatal_decode_error`.)
1450        let mut decoder = LogstashDecoder::new();
1451
1452        let mut first_batch = BytesMut::new();
1453        push_window_size(&mut first_batch, 1);
1454        push_req(&mut first_batch, 1, &[("message", "first window")]);
1455        let decoded =
1456            decode_frames_with_decoder_and_assert_sequences(&mut decoder, first_batch, &[1]);
1457        assert_acknowledgements_for_ready_frames(decoded, &[1], &[1]).await;
1458
1459        let mut second_batch = BytesMut::new();
1460        push_window_size(&mut second_batch, 1);
1461        push_req(
1462            &mut second_batch,
1463            1,
1464            &[("message", "fresh window after completion")],
1465        );
1466        let decoded =
1467            decode_frames_with_decoder_and_assert_sequences(&mut decoder, second_batch, &[1]);
1468        assert_acknowledgements_for_ready_frames(decoded, &[1], &[1]).await;
1469    }
1470
1471    async fn send_req(address: SocketAddr, pairs: &[(&str, &str)], sends_ack: bool) {
1472        let seq = rng().random_range(1..u32::MAX);
1473        let mut socket = tokio::net::TcpStream::connect(address).await.unwrap();
1474
1475        let req = encode_req(seq, pairs);
1476        socket.write_all(&req).await.unwrap();
1477
1478        let mut output = BytesMut::new();
1479        socket.read_buf(&mut output).await.unwrap();
1480
1481        if sends_ack {
1482            assert_eq!(output.get_u8(), b'2');
1483            assert_eq!(output.get_u8(), b'A');
1484            assert_eq!(output.get_u32(), seq);
1485        }
1486        assert_eq!(output.len(), 0);
1487    }
1488
1489    #[test]
1490    fn output_schema_definition_vector_namespace() {
1491        let config = LogstashConfig {
1492            log_namespace: Some(true),
1493            ..Default::default()
1494        };
1495
1496        let definitions = config
1497            .outputs(LogNamespace::Vector)
1498            .remove(0)
1499            .schema_definition(true);
1500
1501        let expected_definition =
1502            Definition::new_with_default_metadata(Kind::bytes(), [LogNamespace::Vector])
1503                .with_meaning(OwnedTargetPath::event_root(), "message")
1504                .with_metadata_field(
1505                    &owned_value_path!("vector", "source_type"),
1506                    Kind::bytes(),
1507                    None,
1508                )
1509                .with_metadata_field(
1510                    &owned_value_path!("vector", "ingest_timestamp"),
1511                    Kind::timestamp(),
1512                    None,
1513                )
1514                .with_metadata_field(
1515                    &owned_value_path!(LogstashConfig::NAME, "timestamp"),
1516                    Kind::timestamp().or_undefined(),
1517                    Some("timestamp"),
1518                )
1519                .with_metadata_field(
1520                    &owned_value_path!(LogstashConfig::NAME, "host"),
1521                    Kind::bytes(),
1522                    Some("host"),
1523                )
1524                .with_metadata_field(
1525                    &owned_value_path!(LogstashConfig::NAME, "tls_client_metadata"),
1526                    Kind::object(Collection::empty().with_unknown(Kind::bytes())).or_undefined(),
1527                    None,
1528                );
1529
1530        assert_eq!(definitions, Some(expected_definition))
1531    }
1532
1533    #[test]
1534    fn output_schema_definition_legacy_namespace() {
1535        let config = LogstashConfig::default();
1536
1537        let definitions = config
1538            .outputs(LogNamespace::Legacy)
1539            .remove(0)
1540            .schema_definition(true);
1541
1542        let expected_definition = Definition::new_with_default_metadata(
1543            Kind::object(Collection::empty()),
1544            [LogNamespace::Legacy],
1545        )
1546        .with_event_field(
1547            &owned_value_path!("message"),
1548            Kind::bytes(),
1549            Some("message"),
1550        )
1551        .with_event_field(&owned_value_path!("source_type"), Kind::bytes(), None)
1552        .with_event_field(&owned_value_path!("timestamp"), Kind::timestamp(), None)
1553        .with_event_field(&owned_value_path!("host"), Kind::bytes(), Some("host"));
1554
1555        assert_eq!(definitions, Some(expected_definition))
1556    }
1557}
1558
1559#[cfg(all(test, feature = "logstash-integration-tests"))]
1560mod integration_tests {
1561    use std::time::Duration;
1562
1563    use futures::Stream;
1564    use tokio::time::timeout;
1565    use vrl::event_path;
1566
1567    use super::*;
1568    use crate::{
1569        SourceSender,
1570        config::SourceContext,
1571        event::EventStatus,
1572        test_util::{
1573            collect_n,
1574            components::{SOCKET_PUSH_SOURCE_TAGS, assert_source_compliance},
1575            wait_for_tcp,
1576        },
1577        tls::{TlsConfig, TlsEnableableConfig},
1578    };
1579
1580    fn heartbeat_address() -> String {
1581        std::env::var("HEARTBEAT_ADDRESS")
1582            .expect("Address of Beats Heartbeat service must be specified.")
1583    }
1584
1585    #[tokio::test]
1586    async fn beats_heartbeat() {
1587        let events = assert_source_compliance(&SOCKET_PUSH_SOURCE_TAGS, async {
1588            let out = source(heartbeat_address(), None).await;
1589
1590            timeout(Duration::from_secs(60), collect_n(out, 1))
1591                .await
1592                .unwrap()
1593        })
1594        .await;
1595
1596        assert!(!events.is_empty());
1597
1598        let log = events[0].as_log();
1599        assert_eq!(
1600            log.get(event_path!("@metadata", "beat")),
1601            Some(String::from("heartbeat").into()).as_ref()
1602        );
1603        assert_eq!(
1604            log.get(event_path!("summary", "up")),
1605            Some(1.into()).as_ref()
1606        );
1607        assert!(log.get(event_path!("timestamp")).is_some());
1608        assert!(log.get(event_path!("host")).is_some());
1609    }
1610
1611    fn logstash_address() -> String {
1612        std::env::var("LOGSTASH_ADDRESS")
1613            .expect("Listen address for `logstash` source must be specified.")
1614    }
1615
1616    #[tokio::test]
1617    async fn logstash() {
1618        let events = assert_source_compliance(&SOCKET_PUSH_SOURCE_TAGS, async {
1619            let out = source(
1620                logstash_address(),
1621                Some(TlsEnableableConfig {
1622                    enabled: Some(true),
1623                    options: TlsConfig {
1624                        crt_file: Some(
1625                            "tests/integration/shared/data/host.docker.internal.crt".into(),
1626                        ),
1627                        key_file: Some(
1628                            "tests/integration/shared/data/host.docker.internal.key".into(),
1629                        ),
1630                        ..Default::default()
1631                    },
1632                }),
1633            )
1634            .await;
1635
1636            timeout(Duration::from_secs(60), collect_n(out, 1))
1637                .await
1638                .unwrap()
1639        })
1640        .await;
1641
1642        assert!(!events.is_empty());
1643
1644        let log = events[0].as_log();
1645        assert!(
1646            log.get(event_path!("line"))
1647                .unwrap()
1648                .to_string_lossy()
1649                .contains("Hello World")
1650        );
1651        assert!(log.get(event_path!("host")).is_some());
1652    }
1653
1654    async fn source(
1655        address: String,
1656        tls: Option<TlsEnableableConfig>,
1657    ) -> impl Stream<Item = Event> + Unpin {
1658        let (sender, recv) = SourceSender::new_test_finalize(EventStatus::Delivered);
1659        let address: SocketAddr = address.parse().unwrap();
1660        let tls_config = TlsSourceConfig {
1661            client_metadata_key: None,
1662            tls_config: tls.unwrap_or_default(),
1663        };
1664        tokio::spawn(async move {
1665            LogstashConfig {
1666                address: address.into(),
1667                tls: Some(tls_config),
1668                keepalive: None,
1669                permit_origin: None,
1670                receive_buffer_bytes: None,
1671                acknowledgements: false.into(),
1672                connection_limit: None,
1673                log_namespace: None,
1674            }
1675            .build(SourceContext::new_test(sender, None))
1676            .await
1677            .unwrap()
1678            .await
1679            .unwrap()
1680        });
1681        wait_for_tcp(address).await;
1682        recv
1683    }
1684}