Skip to main content

vector/sources/
logstash.rs

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