Skip to main content

vector/sources/splunk_hec/
mod.rs

1use std::{
2    collections::HashMap,
3    convert::Infallible,
4    net::{Ipv4Addr, SocketAddr},
5    sync::Arc,
6    time::Duration,
7};
8
9use bytes::{Buf, Bytes, BytesMut};
10use chrono::{DateTime, TimeZone, Utc};
11use futures::FutureExt;
12use http::StatusCode;
13use hyper::{Server, service::make_service_fn};
14use serde::{Serialize, de::DeserializeOwned};
15use serde_json::{
16    Deserializer, Value as JsonValue,
17    de::{Read as JsonRead, StrRead},
18};
19use snafu::Snafu;
20use tokio::net::TcpStream;
21use tokio_util::codec::Decoder as _;
22use tower::ServiceBuilder;
23use tracing::Span;
24use vector_lib::{
25    EstimatedJsonEncodedSizeOf,
26    codecs::{
27        Decoder, StreamDecodingError,
28        decoding::{DeserializerConfig, FramingConfig},
29    },
30    config::{LegacyKey, LogNamespace},
31    configurable::configurable_component,
32    event::{BatchNotifier, BatchStatusReceiver, EventMetadata},
33    internal_event::{
34        ComponentEventsDropped, CountByteSize, InternalEventHandle as _, Registered, UNINTENTIONAL,
35    },
36    lookup::{
37        self, OwnedValuePath, event_path, lookup_v2::OptionalValuePath, metadata_path,
38        owned_value_path,
39    },
40    schema::meaning,
41    sensitive_string::SensitiveString,
42    source_sender::SendError,
43    tls::MaybeTlsIncomingStream,
44};
45use vrl::{
46    path::{OwnedTargetPath, PathPrefix, ValuePath as _},
47    value::{Kind, kind::Collection},
48};
49use warp::{
50    Filter, Reply,
51    filters::BoxedFilter,
52    http::header::{CONTENT_TYPE, HeaderValue},
53    path,
54    reject::Rejection,
55    reply::Response,
56};
57
58use self::{
59    acknowledgements::{
60        HecAckStatusRequest, HecAckStatusResponse, HecAcknowledgementsConfig,
61        IndexerAcknowledgement,
62    },
63    splunk_response::{HecResponse, HecResponseMetadata, HecStatusCode},
64};
65use crate::{
66    SourceSender,
67    codecs::DecodingConfig,
68    common::http::ErrorMessage,
69    config::{DataType, Resource, SourceConfig, SourceContext, SourceOutput, log_schema},
70    event::{Event, LogEvent, Value},
71    http::{KeepaliveConfig, MaxConnectionAgeLayer, build_http_trace_layer},
72    internal_events::{
73        EventsReceived, HttpBytesReceived, SplunkHecRequestBodyInvalidError, SplunkHecRequestError,
74    },
75    serde::bool_or_struct,
76    sources::util::{decompression::CappedDecoder, http::capped_body},
77    tls::{MaybeTlsSettings, TlsEnableableConfig},
78};
79
80mod acknowledgements;
81
82// Event fields unique to splunk_hec source
83pub const CHANNEL: &str = "splunk_channel";
84pub const INDEX: &str = "splunk_index";
85pub const SOURCE: &str = "splunk_source";
86pub const SOURCETYPE: &str = "splunk_sourcetype";
87
88const X_SPLUNK_REQUEST_CHANNEL: &str = "x-splunk-request-channel";
89
90/// Configuration for the `splunk_hec` source.
91#[configurable_component(source("splunk_hec", "Receive logs from Splunk."))]
92#[derive(Clone, Debug)]
93#[serde(deny_unknown_fields, default)]
94pub struct SplunkConfig {
95    /// The socket address to listen for connections on.
96    ///
97    /// The address _must_ include a port.
98    #[serde(default = "default_socket_address")]
99    pub address: SocketAddr,
100
101    /// Optional authorization token.
102    ///
103    /// If supplied, incoming requests must supply this token in the `Authorization` header, just as a client would if
104    /// it was communicating with the Splunk HEC endpoint directly.
105    ///
106    /// If _not_ supplied, the `Authorization` header is ignored and requests are not authenticated.
107    #[configurable(deprecated = "This option has been deprecated, use `valid_tokens` instead.")]
108    token: Option<SensitiveString>,
109
110    /// A list of valid authorization tokens.
111    ///
112    /// If supplied, incoming requests must supply one of these tokens in the `Authorization` header, just as a client
113    /// would if it was communicating with the Splunk HEC endpoint directly.
114    ///
115    /// If _not_ supplied, the `Authorization` header is ignored and requests are not authenticated.
116    #[configurable(metadata(docs::examples = "A94A8FE5CCB19BA61C4C08"))]
117    valid_tokens: Option<Vec<SensitiveString>>,
118
119    /// Whether or not to forward the Splunk HEC authentication token with events.
120    ///
121    /// If set to `true`, when incoming requests contain a Splunk HEC token, the token used is kept in the
122    /// event metadata and preferentially used if the event is sent to a Splunk HEC sink.
123    store_hec_token: bool,
124
125    #[configurable(derived)]
126    tls: Option<TlsEnableableConfig>,
127
128    #[configurable(derived)]
129    #[serde(deserialize_with = "bool_or_struct")]
130    acknowledgements: HecAcknowledgementsConfig,
131
132    /// The namespace to use for logs. This overrides the global settings.
133    #[configurable(metadata(docs::hidden))]
134    #[serde(default)]
135    log_namespace: Option<bool>,
136
137    #[configurable(derived)]
138    #[serde(default)]
139    keepalive: KeepaliveConfig,
140
141    /// Codec configuration applied to events received on `/services/collector/event`.
142    ///
143    /// When `decoding` is set, Vector applies a second decoding pass after parsing the
144    /// HEC envelope. The envelope's `event` field is passed through the codec,
145    /// and a single envelope can fan out to multiple events. Decode failures are
146    /// swallowed and do not return an error to the Splunk client.
147    ///
148    /// The VRL codec can access HEC envelope metadata, such as host, sourcetype, and,
149    /// channel, and the authentication token via `%splunk_hec.*` paths and
150    /// `get_secret!("splunk_hec_token")` before the program executes.
151    #[configurable(derived)]
152    #[configurable(metadata(docs::advanced))]
153    #[serde(default)]
154    pub event: CodecConfig,
155
156    /// Codec configuration applied to events received on `/services/collector/raw`.
157    ///
158    /// When `decoding` is set, the (decompressed) request body is fed through the
159    /// codec instead of being emitted as a single event. Decode failures are
160    /// swallowed and do not return an error to the Splunk client. When unset, the
161    /// endpoint preserves its existing behavior of one event per request body.
162    #[configurable(derived)]
163    #[configurable(metadata(docs::advanced))]
164    #[serde(default)]
165    pub raw: CodecConfig,
166}
167
168/// Codec configuration applied to one of the `splunk_hec` endpoints.
169#[configurable_component]
170#[derive(Clone, Debug, Default)]
171#[serde(deny_unknown_fields, default)]
172pub struct CodecConfig {
173    /// Framing configuration applied to the payload.
174    ///
175    /// Only used when `decoding` is also set. Defaults to a per-codec choice
176    /// (typically `bytes`) that produces one event per payload.
177    #[configurable(derived)]
178    #[configurable(metadata(docs::advanced))]
179    #[serde(default)]
180    pub framing: Option<FramingConfig>,
181
182    /// Decoding configuration applied to the payload.
183    ///
184    /// When unset, the endpoint preserves its existing per-endpoint default
185    /// behavior. When set, the endpoint-selected payload is processed through
186    /// `framing` and `decoding`, and a single payload can fan out to multiple
187    /// events.
188    #[configurable(derived)]
189    #[configurable(metadata(docs::advanced))]
190    #[serde(default)]
191    pub decoding: Option<DeserializerConfig>,
192}
193
194impl CodecConfig {
195    fn build_decoder(&self, log_namespace: LogNamespace) -> crate::Result<Option<Decoder>> {
196        match &self.decoding {
197            Some(decoding) => {
198                let framing = self
199                    .framing
200                    .clone()
201                    .unwrap_or_else(|| decoding.default_message_based_framing());
202                Ok(Some(
203                    DecodingConfig::new(framing, decoding.clone(), log_namespace).build()?,
204                ))
205            }
206            None => Ok(None),
207        }
208    }
209}
210
211impl_generate_config_from_default!(SplunkConfig);
212
213impl Default for SplunkConfig {
214    fn default() -> Self {
215        SplunkConfig {
216            address: default_socket_address(),
217            token: None,
218            valid_tokens: None,
219            tls: None,
220            acknowledgements: Default::default(),
221            store_hec_token: false,
222            log_namespace: None,
223            keepalive: Default::default(),
224            event: CodecConfig::default(),
225            raw: CodecConfig::default(),
226        }
227    }
228}
229
230fn default_socket_address() -> SocketAddr {
231    SocketAddr::new(Ipv4Addr::UNSPECIFIED.into(), 8088)
232}
233
234#[async_trait::async_trait]
235#[typetag::serde(name = "splunk_hec")]
236impl SourceConfig for SplunkConfig {
237    async fn build(&self, cx: SourceContext) -> crate::Result<super::Source> {
238        let tls = MaybeTlsSettings::from_config(self.tls.as_ref(), true)?;
239        let shutdown = cx.shutdown.clone();
240        let out = cx.out.clone();
241        let log_namespace = cx.log_namespace(self.log_namespace);
242        let event_decoder = self.event.build_decoder(log_namespace)?;
243        let raw_decoder = self.raw.build_decoder(log_namespace)?;
244        let source = SplunkSource::new(
245            self,
246            tls.http_protocol_name(),
247            event_decoder,
248            raw_decoder,
249            cx,
250        );
251
252        let event_service = source.event_service(out.clone());
253        let raw_service = source.raw_service(out);
254        let health_service = source.health_service();
255        let ack_service = source.ack_service();
256        let options = SplunkSource::options();
257
258        let services = path!("services" / "collector" / ..)
259            .and(
260                event_service
261                    .or(raw_service)
262                    .unify()
263                    .or(health_service)
264                    .unify()
265                    .or(ack_service)
266                    .unify()
267                    .or(options)
268                    .unify(),
269            )
270            .or_else(finish_err);
271
272        let listener = tls.bind(&self.address).await?;
273
274        let keepalive_settings = self.keepalive.clone();
275        Ok(Box::pin(async move {
276            let span = Span::current();
277            let make_svc = make_service_fn(move |conn: &MaybeTlsIncomingStream<TcpStream>| {
278                let svc = ServiceBuilder::new()
279                    .layer(build_http_trace_layer(span.clone()))
280                    .option_layer(keepalive_settings.max_connection_age_secs.map(|secs| {
281                        MaxConnectionAgeLayer::new(
282                            Duration::from_secs(secs),
283                            keepalive_settings.max_connection_age_jitter_factor,
284                            conn.peer_addr(),
285                        )
286                    }))
287                    .service(warp::service(services.clone()));
288                futures_util::future::ok::<_, Infallible>(svc)
289            });
290
291            Server::builder(hyper::server::accept::from_stream(listener.accept_stream()))
292                .serve(make_svc)
293                .with_graceful_shutdown(shutdown.map(|_| ()))
294                .await
295                .map_err(|err| {
296                    error!("An error occurred: {:?}.", err);
297                })?;
298
299            Ok(())
300        }))
301    }
302
303    fn outputs(&self, global_log_namespace: LogNamespace) -> Vec<SourceOutput> {
304        let log_namespace = global_log_namespace.merge(self.log_namespace);
305
306        // Build schemas per endpoint, then merge them. Each endpoint decides at
307        // runtime whether source metadata overwrites event fields or defers to a
308        // decoder-produced value, so applying one global strategy would make mixed
309        // decoder/no-decoder configurations advertise the wrong contract.
310        let legacy_base = || match log_namespace {
311            LogNamespace::Legacy => {
312                let definition = vector_lib::schema::Definition::empty_legacy_namespace()
313                    .with_event_field(
314                        &owned_value_path!("line"),
315                        Kind::object(Collection::empty())
316                            .or_array(Collection::empty())
317                            .or_undefined(),
318                        None,
319                    );
320
321                if let Some(message_key) = log_schema().message_key() {
322                    definition.with_event_field(
323                        message_key,
324                        Kind::bytes().or_undefined(),
325                        Some(meaning::MESSAGE),
326                    )
327                } else {
328                    definition
329                }
330            }
331            LogNamespace::Vector => vector_lib::schema::Definition::new_with_default_metadata(
332                Kind::bytes().or_object(Collection::empty()),
333                [log_namespace],
334            )
335            .with_meaning(OwnedTargetPath::event_root(), meaning::MESSAGE),
336        };
337
338        let endpoint_base = |decoding: &Option<DeserializerConfig>| match decoding {
339            Some(decoding) => decoding.schema_definition(log_namespace),
340            None => legacy_base(),
341        };
342
343        let splunk_legacy_key = |path: OwnedValuePath, has_decoder: bool| {
344            if has_decoder {
345                LegacyKey::InsertIfEmpty(path)
346            } else {
347                LegacyKey::Overwrite(path)
348            }
349        };
350
351        let add_common_metadata = |definition: vector_lib::schema::Definition| {
352            definition
353                .with_standard_vector_source_metadata()
354                .with_source_metadata(
355                    SplunkConfig::NAME,
356                    log_schema()
357                        .host_key()
358                        .cloned()
359                        .map(LegacyKey::InsertIfEmpty),
360                    &owned_value_path!("host"),
361                    Kind::bytes(),
362                    Some(meaning::HOST),
363                )
364        };
365
366        let add_channel_metadata = |definition: vector_lib::schema::Definition,
367                                    has_decoder: bool| {
368            definition.with_source_metadata(
369                SplunkConfig::NAME,
370                Some(splunk_legacy_key(owned_value_path!(CHANNEL), has_decoder)),
371                &owned_value_path!("channel"),
372                Kind::bytes(),
373                None,
374            )
375        };
376
377        let event_has_decoder = self.event.decoding.is_some();
378        let raw_has_decoder = self.raw.decoding.is_some();
379
380        // Merge the per-endpoint base schemas (event root kind + standard Vector
381        // metadata). Splunk-specific fields are added once afterward with
382        // per-field decoder flags, avoiding the widening that occurs when the
383        // raw schema's open `metadata_kind.unknown` overrides specific fields
384        // from the event schema during merge.
385        let merged_base = add_common_metadata(
386            endpoint_base(&self.event.decoding).merge(endpoint_base(&self.raw.decoding)),
387        );
388
389        // `index`, `source`, `sourcetype` are only written by the /event endpoint.
390        // `channel` is written by both; use Overwrite if either endpoint has no
391        // decoder (some events still overwrite it).
392        let channel_has_decoder = event_has_decoder && raw_has_decoder;
393        let schema_definition = add_channel_metadata(
394            merged_base
395                .with_source_metadata(
396                    SplunkConfig::NAME,
397                    Some(splunk_legacy_key(
398                        owned_value_path!(INDEX),
399                        event_has_decoder,
400                    )),
401                    &owned_value_path!("index"),
402                    Kind::bytes(),
403                    None,
404                )
405                .with_source_metadata(
406                    SplunkConfig::NAME,
407                    Some(splunk_legacy_key(
408                        owned_value_path!(SOURCE),
409                        event_has_decoder,
410                    )),
411                    &owned_value_path!("source"),
412                    Kind::bytes(),
413                    Some(meaning::SERVICE),
414                )
415                // Not to be confused with `source_type`.
416                .with_source_metadata(
417                    SplunkConfig::NAME,
418                    Some(splunk_legacy_key(
419                        owned_value_path!(SOURCETYPE),
420                        event_has_decoder,
421                    )),
422                    &owned_value_path!("sourcetype"),
423                    Kind::bytes(),
424                    None,
425                ),
426            channel_has_decoder,
427        );
428
429        // Output type is the union of both endpoints' decoder output types
430        // (logs from a JSON codec, metrics from native, etc.). The legacy path
431        // always emits logs, so when an endpoint has no decoder we OR `Log` in.
432        let output_type = match (&self.event.decoding, &self.raw.decoding) {
433            (None, None) => DataType::Log,
434            (Some(d), None) | (None, Some(d)) => d.output_type() | DataType::Log,
435            (Some(de), Some(dr)) => de.output_type() | dr.output_type(),
436        };
437        vec![SourceOutput::new_maybe_logs(output_type, schema_definition)]
438    }
439
440    fn resources(&self) -> Vec<Resource> {
441        vec![Resource::tcp(self.address)]
442    }
443
444    fn can_acknowledge(&self) -> bool {
445        true
446    }
447}
448
449/// Shared data for responding to requests.
450struct SplunkSource {
451    valid_credentials: Vec<String>,
452    protocol: &'static str,
453    idx_ack: Option<Arc<IndexerAcknowledgement>>,
454    store_hec_token: bool,
455    log_namespace: LogNamespace,
456    events_received: Registered<EventsReceived>,
457    event_decoder: Option<Decoder>,
458    raw_decoder: Option<Decoder>,
459}
460
461impl SplunkSource {
462    fn new(
463        config: &SplunkConfig,
464        protocol: &'static str,
465        event_decoder: Option<Decoder>,
466        raw_decoder: Option<Decoder>,
467        cx: SourceContext,
468    ) -> Self {
469        let log_namespace = cx.log_namespace(config.log_namespace);
470        let acknowledgements = cx.do_acknowledgements(config.acknowledgements.enabled.into());
471        let shutdown = cx.shutdown;
472        let valid_tokens = config
473            .valid_tokens
474            .iter()
475            .flatten()
476            .chain(config.token.iter());
477
478        let idx_ack = acknowledgements.then(|| {
479            Arc::new(IndexerAcknowledgement::new(
480                config.acknowledgements.clone(),
481                shutdown,
482            ))
483        });
484
485        SplunkSource {
486            valid_credentials: valid_tokens
487                .map(|token| format!("Splunk {}", token.inner()))
488                .collect(),
489            protocol,
490            idx_ack,
491            store_hec_token: config.store_hec_token,
492            log_namespace,
493            events_received: register!(EventsReceived),
494            event_decoder,
495            raw_decoder,
496        }
497    }
498
499    fn event_service(&self, out: SourceSender) -> BoxedFilter<(Response,)> {
500        let splunk_channel_query_param = warp::query::<HashMap<String, String>>()
501            .map(|qs: HashMap<String, String>| qs.get("channel").map(|v| v.to_owned()));
502        let splunk_channel_header = warp::header::optional::<String>(X_SPLUNK_REQUEST_CHANNEL);
503
504        let splunk_channel = splunk_channel_header
505            .and(splunk_channel_query_param)
506            .map(|header: Option<String>, query_param| header.or(query_param));
507
508        let protocol = self.protocol;
509        let idx_ack = self.idx_ack.clone();
510        let store_hec_token = self.store_hec_token;
511        let log_namespace = self.log_namespace;
512        let events_received = self.events_received.clone();
513        let decoder = self.event_decoder.clone();
514
515        warp::post()
516            .and(
517                path!("event")
518                    .or(path!("event" / "1.0"))
519                    .or(warp::path::end()),
520            )
521            .and(self.authorization())
522            .and(splunk_channel)
523            .and(warp::addr::remote())
524            .and(warp::header::optional::<String>("X-Forwarded-For"))
525            .and(self.gzip())
526            .and(capped_body())
527            .and(warp::path::full())
528            .and_then(
529                move |_,
530                      token: Option<String>,
531                      channel: Option<String>,
532                      remote: Option<SocketAddr>,
533                      remote_addr: Option<String>,
534                      gzip: bool,
535                      body: Bytes,
536                      path: warp::path::FullPath| {
537                    let mut out = out.clone();
538                    let idx_ack = idx_ack.clone();
539                    let events_received = events_received.clone();
540                    let decoder = decoder.clone();
541
542                    async move {
543                        if idx_ack.is_some() && channel.is_none() {
544                            return Err(Rejection::from(ApiError::MissingChannel));
545                        }
546
547                        let data;
548                        let (byte_size, body) = if gzip {
549                            // Cap the decompressed output to mitigate gzip-bomb DoS.
550                            data = CappedDecoder::gzip(body.reader())
551                                .decompress()
552                                .map_err(|_| Rejection::from(ApiError::BadRequest))?;
553                            (data.len(), String::from_utf8_lossy(data.as_slice()))
554                        } else {
555                            (body.len(), String::from_utf8_lossy(body.as_ref()))
556                        };
557                        emit!(HttpBytesReceived {
558                            byte_size,
559                            http_path: path.as_str(),
560                            protocol,
561                        });
562
563                        let (batch, mut receiver) =
564                            BatchNotifier::maybe_new_with_receiver(idx_ack.is_some());
565                        let decoder_in_use = decoder.is_some();
566
567                        // Without a decoder, register the ack id BEFORE iteration so
568                        // capacity-exhaustion (`ServiceUnavailable`) short-circuits
569                        // the request without parsing the body - byte-for-byte parity
570                        // with the pre-decoder behavior.
571                        let mut maybe_ack_id = None;
572                        if !decoder_in_use {
573                            maybe_ack_id =
574                                register_ack(idx_ack.clone(), receiver.take(), channel.clone())
575                                    .await?;
576                        }
577
578                        let mut error = None;
579                        let mut events = Vec::new();
580                        let mut had_decode_errors = false;
581
582                        let iter: EventIterator<'_, StrRead<'_>> = EventIteratorGenerator {
583                            deserializer: Deserializer::from_str(&body).into_iter::<JsonValue>(),
584                            channel: channel.clone(),
585                            remote,
586                            remote_addr,
587                            batch,
588                            token: token.filter(|_| store_hec_token).map(Into::into),
589                            log_namespace,
590                            events_received,
591                            decoder,
592                        }
593                        .into();
594
595                        for result in iter {
596                            match result {
597                                Ok((chunk, errored)) => {
598                                    events.extend(chunk);
599                                    had_decode_errors |= errored;
600                                }
601                                Err(err) => {
602                                    error = Some(err);
603                                    break;
604                                }
605                            }
606                        }
607
608                        // With a decoder, defer ack registration until we know whether
609                        // the codec emitted anything *and* whether it dropped any
610                        // frames. Also skip ack registration when a later envelope
611                        // errored even if earlier ones produced events: the client
612                        // gets a 400 and never sees the ack id, so registering it
613                        // only leaks pending-ack capacity.
614                        if decoder_in_use {
615                            maybe_ack_id =
616                                if events.is_empty() || had_decode_errors || error.is_some() {
617                                    drop(receiver);
618                                    None
619                                } else {
620                                    register_ack(idx_ack, receiver, channel).await?
621                                };
622                        }
623
624                        if !events.is_empty() {
625                            match out.send_batch(events).await {
626                                Ok(()) => (),
627                                Err(SendError::Closed) => {
628                                    return Err(Rejection::from(ApiError::ServerShutdown));
629                                }
630                                Err(SendError::Timeout) => {
631                                    unreachable!("No timeout is configured for this source.")
632                                }
633                            }
634                        }
635
636                        if let Some(error) = error {
637                            Err(error)
638                        } else {
639                            Ok(maybe_ack_id)
640                        }
641                    }
642                },
643            )
644            .map(finish_ok)
645            .boxed()
646    }
647
648    fn raw_service(&self, out: SourceSender) -> BoxedFilter<(Response,)> {
649        let protocol = self.protocol;
650        let idx_ack = self.idx_ack.clone();
651        let store_hec_token = self.store_hec_token;
652        let events_received = self.events_received.clone();
653        let log_namespace = self.log_namespace;
654        let decoder = self.raw_decoder.clone();
655
656        warp::post()
657            .and(path!("raw" / "1.0").or(path!("raw")))
658            .and(self.authorization())
659            .and(SplunkSource::required_channel())
660            .and(warp::addr::remote())
661            .and(warp::header::optional::<String>("X-Forwarded-For"))
662            .and(self.gzip())
663            .and(capped_body())
664            .and(warp::path::full())
665            .and_then(
666                move |_,
667                      token: Option<String>,
668                      channel_id: String,
669                      remote: Option<SocketAddr>,
670                      xff: Option<String>,
671                      gzip: bool,
672                      body: Bytes,
673                      path: warp::path::FullPath| {
674                    let mut out = out.clone();
675                    let idx_ack = idx_ack.clone();
676                    let events_received = events_received.clone();
677                    let decoder = decoder.clone();
678                    emit!(HttpBytesReceived {
679                        byte_size: body.len(),
680                        http_path: path.as_str(),
681                        protocol,
682                    });
683
684                    async move {
685                        let (batch, receiver) =
686                            BatchNotifier::maybe_new_with_receiver(idx_ack.is_some());
687
688                        // No-decoder path: byte-for-byte identical to the pre-decoder
689                        // code - register ack first (fast-fail under capacity
690                        // exhaustion), build a single event, send via `send_event`
691                        // (avoids `send_batch_latency` emission).
692                        let Some(decoder) = decoder else {
693                            let maybe_ack_id =
694                                register_ack(idx_ack, receiver, Some(channel_id.clone())).await?;
695                            let (mut events, _) = raw_event(
696                                body,
697                                gzip,
698                                channel_id,
699                                remote,
700                                xff,
701                                batch,
702                                log_namespace,
703                                &events_received,
704                                None,
705                                None,
706                            )?;
707                            // raw_event with no decoder always produces exactly one
708                            // event.
709                            let mut event = events.pop().expect(
710                                "raw_event always produces a single event when no decoder is set",
711                            );
712                            if let Some(token) = token.filter(|_| store_hec_token) {
713                                event.metadata_mut().set_splunk_hec_token(token.into());
714                            }
715                            let res = out.send_event(event).await;
716                            return res
717                                .map(|_| maybe_ack_id)
718                                .map_err(|_| Rejection::from(ApiError::ServerShutdown));
719                        };
720
721                        // Decoder path: pass the optional HEC token into raw_event so
722                        // it's stamped on each event the moment it leaves the codec
723                        // (rather than after the whole payload is decoded).
724                        let token: Option<Arc<str>> =
725                            token.filter(|_| store_hec_token).map(Arc::from);
726                        let (events, had_decode_errors) = raw_event(
727                            body,
728                            gzip,
729                            channel_id.clone(),
730                            remote,
731                            xff,
732                            batch,
733                            log_namespace,
734                            &events_received,
735                            Some(decoder),
736                            token,
737                        )?;
738
739                        if events.is_empty() || had_decode_errors {
740                            // With newline framing, `valid \n invalid \n valid`
741                            // decodes to two events plus one dropped frame; returning
742                            // an ack id there would let `/services/collector/ack`
743                            // report success for data Vector silently lost.
744                            drop(receiver);
745                            if events.is_empty() {
746                                return Ok(None);
747                            }
748                            // Forward the partial events with no ack so the source's
749                            // existing partial-delivery semantics still apply.
750                            let res = out.send_batch(events).await;
751                            return res
752                                .map(|_| None)
753                                .map_err(|_| Rejection::from(ApiError::ServerShutdown));
754                        }
755
756                        let maybe_ack_id =
757                            register_ack(idx_ack, receiver, Some(channel_id)).await?;
758
759                        let res = out.send_batch(events).await;
760                        res.map(|_| maybe_ack_id)
761                            .map_err(|_| Rejection::from(ApiError::ServerShutdown))
762                    }
763                },
764            )
765            .map(finish_ok)
766            .boxed()
767    }
768
769    fn health_service(&self) -> BoxedFilter<(Response,)> {
770        // The Splunk docs document this endpoint as returning a 400 if given an invalid Splunk
771        // token, but, in practice, it seems to ignore the token altogether
772        //
773        // The response body was taken from Splunk 8.2.4
774        //
775        // https://docs.splunk.com/Documentation/Splunk/8.2.5/RESTREF/RESTinput#services.2Fcollector.2Fhealth
776        warp::get()
777            .and(path!("health" / "1.0").or(path!("health")))
778            .map(move |_| {
779                http::Response::builder()
780                    .header(http::header::CONTENT_TYPE, "application/json")
781                    .body(hyper::Body::from(r#"{"text":"HEC is healthy","code":17}"#))
782                    .expect("static response")
783            })
784            .boxed()
785    }
786
787    fn lenient_json_content_type_check<T>() -> impl Filter<Extract = (T,), Error = Rejection> + Clone
788    where
789        T: Send + DeserializeOwned + 'static,
790    {
791        warp::header::optional::<HeaderValue>(CONTENT_TYPE.as_str())
792            .and(capped_body())
793            .and_then(
794                |ctype: Option<HeaderValue>, body: bytes::Bytes| async move {
795                    let ok = ctype
796                        .as_ref()
797                        .and_then(|v| v.to_str().ok())
798                        .map(|h| h.to_ascii_lowercase().contains("application/json"))
799                        .unwrap_or(true);
800
801                    if !ok {
802                        return Err(warp::reject::custom(ApiError::UnsupportedContentType));
803                    }
804
805                    let value = serde_json::from_slice::<T>(&body)
806                        .map_err(|_| warp::reject::custom(ApiError::BadRequest))?;
807
808                    Ok(value)
809                },
810            )
811    }
812
813    fn ack_service(&self) -> BoxedFilter<(Response,)> {
814        let idx_ack = self.idx_ack.clone();
815
816        warp::post()
817            .and(warp::path!("ack"))
818            .and(self.authorization())
819            .and(SplunkSource::required_channel())
820            .and(Self::lenient_json_content_type_check::<HecAckStatusRequest>())
821            .and_then(move |_, channel: String, req: HecAckStatusRequest| {
822                let idx_ack = idx_ack.clone();
823                async move {
824                    if let Some(idx_ack) = idx_ack {
825                        let acks = idx_ack
826                            .get_acks_status_from_channel(channel, &req.acks)
827                            .await?;
828                        Ok(warp::reply::json(&HecAckStatusResponse { acks }).into_response())
829                    } else {
830                        Err(warp::reject::custom(ApiError::AckIsDisabled))
831                    }
832                }
833            })
834            .boxed()
835    }
836
837    fn options() -> BoxedFilter<(Response,)> {
838        let post = warp::options()
839            .and(
840                path!("event")
841                    .or(path!("event" / "1.0"))
842                    .or(path!("raw" / "1.0"))
843                    .or(path!("raw")),
844            )
845            .map(|_| warp::reply::with_header(warp::reply(), "Allow", "POST").into_response());
846
847        let get = warp::options()
848            .and(path!("health").or(path!("health" / "1.0")))
849            .map(|_| warp::reply::with_header(warp::reply(), "Allow", "GET").into_response());
850
851        post.or(get).unify().boxed()
852    }
853
854    /// Authorize request
855    fn authorization(&self) -> BoxedFilter<(Option<String>,)> {
856        let valid_credentials = self.valid_credentials.clone();
857        warp::header::optional("Authorization")
858            .and_then(move |token: Option<String>| {
859                let valid_credentials = valid_credentials.clone();
860                async move {
861                    match (token, valid_credentials.is_empty()) {
862                        // Remove the "Splunk " prefix if present as it is not
863                        // part of the token itself
864                        (token, true) => {
865                            Ok(token
866                                .map(|t| t.strip_prefix("Splunk ").map(Into::into).unwrap_or(t)))
867                        }
868                        (Some(token), false) if valid_credentials.contains(&token) => Ok(Some(
869                            token
870                                .strip_prefix("Splunk ")
871                                .map(Into::into)
872                                .unwrap_or(token),
873                        )),
874                        (Some(_), false) => Err(Rejection::from(ApiError::InvalidAuthorization)),
875                        (None, false) => Err(Rejection::from(ApiError::MissingAuthorization)),
876                    }
877                }
878            })
879            .boxed()
880    }
881
882    /// Is body encoded with gzip
883    fn gzip(&self) -> BoxedFilter<(bool,)> {
884        warp::header::optional::<String>("Content-Encoding")
885            .and_then(|encoding: Option<String>| async move {
886                match encoding {
887                    Some(s) if s.as_bytes() == b"gzip" => Ok(true),
888                    Some(_) => Err(Rejection::from(ApiError::UnsupportedEncoding)),
889                    None => Ok(false),
890                }
891            })
892            .boxed()
893    }
894
895    fn required_channel() -> BoxedFilter<(String,)> {
896        let splunk_channel_query_param = warp::query::<HashMap<String, String>>()
897            .map(|qs: HashMap<String, String>| qs.get("channel").map(|v| v.to_owned()));
898        let splunk_channel_header = warp::header::optional::<String>(X_SPLUNK_REQUEST_CHANNEL);
899
900        splunk_channel_header
901            .and(splunk_channel_query_param)
902            .and_then(|header: Option<String>, query_param| async move {
903                header
904                    .or(query_param)
905                    .ok_or_else(|| Rejection::from(ApiError::MissingChannel))
906            })
907            .boxed()
908    }
909}
910/// Constructs one or more events from json-s coming from reader.
911/// If errors, it's done with input.
912struct EventIterator<'de, R: JsonRead<'de>> {
913    /// Remaining request with JSON events
914    deserializer: serde_json::StreamDeserializer<'de, R, JsonValue>,
915    /// Count of HEC envelopes (not fan-out events) processed so far. Used both as the
916    /// `InvalidEventNumber` index in Splunk error responses (zero-indexed: subtract 1
917    /// for build-time errors, use as-is for parse errors that haven't entered build)
918    /// and as the "did we see any envelope?" check that gates the `NoData` error.
919    envelopes_processed: usize,
920    /// Optional channel from headers
921    channel: Option<Value>,
922    /// Default time
923    time: Time,
924    /// Remaining extracted default values
925    extractors: [DefaultExtractor; 4],
926    /// Event finalization
927    batch: Option<BatchNotifier>,
928    /// Splunk HEC Token for passthrough
929    token: Option<Arc<str>>,
930    /// Lognamespace to put the events in
931    log_namespace: LogNamespace,
932    /// handle to EventsReceived registry
933    events_received: Registered<EventsReceived>,
934    /// Optional second-stage decoder applied to the envelope payload after HEC
935    /// envelope parsing.
936    decoder: Option<Decoder>,
937}
938
939/// Intermediate struct to generate an `EventIterator`
940struct EventIteratorGenerator<'de, R: JsonRead<'de>> {
941    deserializer: serde_json::StreamDeserializer<'de, R, JsonValue>,
942    channel: Option<String>,
943    batch: Option<BatchNotifier>,
944    token: Option<Arc<str>>,
945    log_namespace: LogNamespace,
946    events_received: Registered<EventsReceived>,
947    remote: Option<SocketAddr>,
948    remote_addr: Option<String>,
949    decoder: Option<Decoder>,
950}
951
952impl<'de, R: JsonRead<'de>> From<EventIteratorGenerator<'de, R>> for EventIterator<'de, R> {
953    fn from(f: EventIteratorGenerator<'de, R>) -> Self {
954        // The host field can collide with decoder-produced output in legacy namespace
955        // (its legacy key is `log_schema().host_key()`, typically `"host"`). When a
956        // decoder is configured, prefer the decoder's value over the envelope's so the
957        // user's parsed view wins on conflict. With no decoder configured, behavior is
958        // unchanged: every extractor uses `Overwrite`.
959        let extractor_strategy = if f.decoder.is_some() {
960            LegacyKeyStrategy::InsertIfEmpty
961        } else {
962            LegacyKeyStrategy::Overwrite
963        };
964        Self {
965            deserializer: f.deserializer,
966            envelopes_processed: 0,
967            channel: f.channel.map(Value::from),
968            time: Time::Now(Utc::now()),
969            extractors: [
970                // Extract the host field with the given priority:
971                // 1. The host field is present in the event payload
972                // 2. The x-forwarded-for header is present in the incoming request
973                // 3. Use the `remote`: SocketAddr value provided by warp
974                DefaultExtractor::new_with(
975                    "host",
976                    log_schema().host_key().cloned().into(),
977                    f.remote_addr
978                        .or_else(|| f.remote.map(|addr| addr.to_string()))
979                        .map(Value::from),
980                    f.log_namespace,
981                )
982                .with_legacy_key_strategy(extractor_strategy),
983                DefaultExtractor::new("index", OptionalValuePath::new(INDEX), f.log_namespace)
984                    .with_legacy_key_strategy(extractor_strategy),
985                DefaultExtractor::new("source", OptionalValuePath::new(SOURCE), f.log_namespace)
986                    .with_legacy_key_strategy(extractor_strategy),
987                DefaultExtractor::new(
988                    "sourcetype",
989                    OptionalValuePath::new(SOURCETYPE),
990                    f.log_namespace,
991                )
992                .with_legacy_key_strategy(extractor_strategy),
993            ],
994            batch: f.batch,
995            token: f.token,
996            log_namespace: f.log_namespace,
997            events_received: f.events_received,
998            decoder: f.decoder,
999        }
1000    }
1001}
1002
1003impl<'de, R: JsonRead<'de>> EventIterator<'de, R> {
1004    /// Process the envelope's `time` field, updating `self.time` (sticky across envelopes
1005    /// when not explicitly provided).
1006    fn process_time(&mut self, json: &mut JsonValue) -> Result<(), Rejection> {
1007        let parsed_time = match json.get_mut("time").map(JsonValue::take) {
1008            Some(JsonValue::Number(time)) => Some(Some(time)),
1009            Some(JsonValue::String(time)) => Some(time.parse::<serde_json::Number>().ok()),
1010            _ => None,
1011        };
1012
1013        match parsed_time {
1014            None => Ok(()),
1015            Some(Some(t)) => {
1016                if let Some(t) = t.as_u64() {
1017                    let time = parse_timestamp(t as i64).ok_or(ApiError::InvalidDataFormat {
1018                        event: self.envelopes_processed.saturating_sub(1),
1019                    })?;
1020                    self.time = Time::Provided(time);
1021                    Ok(())
1022                } else if let Some(t) = t.as_f64() {
1023                    self.time = Time::Provided(
1024                        Utc.timestamp_opt(
1025                            t.floor() as i64,
1026                            (t.fract() * 1000.0 * 1000.0 * 1000.0) as u32,
1027                        )
1028                        .single()
1029                        .expect("invalid timestamp"),
1030                    );
1031                    Ok(())
1032                } else {
1033                    Err(ApiError::InvalidDataFormat {
1034                        event: self.envelopes_processed.saturating_sub(1),
1035                    }
1036                    .into())
1037                }
1038            }
1039            Some(None) => Err(ApiError::InvalidDataFormat {
1040                event: self.envelopes_processed.saturating_sub(1),
1041            }
1042            .into()),
1043        }
1044    }
1045
1046    fn build_event(&mut self, mut json: JsonValue) -> Result<Event, Rejection> {
1047        self.envelopes_processed += 1;
1048        // Construct Event from parsed json event
1049        let mut log = match self.log_namespace {
1050            LogNamespace::Vector => self.build_log_vector(&mut json)?,
1051            LogNamespace::Legacy => self.build_log_legacy(&mut json)?,
1052        };
1053
1054        // Add source type
1055        self.log_namespace.insert_vector_metadata(
1056            &mut log,
1057            log_schema().source_type_key(),
1058            &owned_value_path!("source_type"),
1059            SplunkConfig::NAME,
1060        );
1061
1062        // Process channel field
1063        let channel_path = owned_value_path!(CHANNEL);
1064        if let Some(JsonValue::String(guid)) = json.get_mut("channel").map(JsonValue::take) {
1065            self.log_namespace.insert_source_metadata(
1066                SplunkConfig::NAME,
1067                &mut log,
1068                Some(LegacyKey::Overwrite(&channel_path)),
1069                lookup::path!(CHANNEL),
1070                guid,
1071            );
1072        } else if let Some(guid) = self.channel.as_ref() {
1073            self.log_namespace.insert_source_metadata(
1074                SplunkConfig::NAME,
1075                &mut log,
1076                Some(LegacyKey::Overwrite(&channel_path)),
1077                lookup::path!(CHANNEL),
1078                guid.clone(),
1079            );
1080        }
1081
1082        // Process fields field
1083        if let Some(JsonValue::Object(object)) = json.get_mut("fields").map(JsonValue::take) {
1084            for (key, value) in object {
1085                self.log_namespace.insert_source_metadata(
1086                    SplunkConfig::NAME,
1087                    &mut log,
1088                    Some(LegacyKey::Overwrite(&owned_value_path!(key.as_str()))),
1089                    lookup::path!(key.as_str()),
1090                    value,
1091                );
1092            }
1093        }
1094
1095        self.process_time(&mut json)?;
1096
1097        // Add time field
1098        let timestamp = match self.time.clone() {
1099            Time::Provided(time) => time,
1100            Time::Now(time) => time,
1101        };
1102
1103        self.log_namespace.insert_source_metadata(
1104            SplunkConfig::NAME,
1105            &mut log,
1106            log_schema().timestamp_key().map(LegacyKey::Overwrite),
1107            lookup::path!("timestamp"),
1108            timestamp,
1109        );
1110
1111        // Extract default extracted fields
1112        for de in self.extractors.iter_mut() {
1113            de.extract(&mut log, &mut json);
1114        }
1115
1116        // Add passthrough token if present
1117        if let Some(token) = &self.token {
1118            log.metadata_mut().set_splunk_hec_token(Arc::clone(token));
1119        }
1120
1121        if let Some(batch) = self.batch.clone() {
1122            log = log.with_batch_notifier(&batch);
1123        }
1124
1125        Ok(log.into())
1126    }
1127
1128    /// Build an `EventMetadata` template from the current envelope context so
1129    /// that VRL decoders can read source-supplied values via `%`-prefixed paths
1130    /// before the decoder program executes.
1131    ///
1132    /// Peeks at the envelope `json` without consuming any fields (consumption
1133    /// happens later in `build_events_decoded`). Falls back to sticky extractor
1134    /// state for fields not present in the current envelope.
1135    fn build_vrl_metadata(&self, json: &JsonValue) -> EventMetadata {
1136        let mut metadata = EventMetadata::default();
1137
1138        // Splunk HEC token as a secret so VRL can read it via get_secret!()
1139        if let Some(token) = &self.token {
1140            metadata.set_splunk_hec_token(Arc::clone(token));
1141        }
1142
1143        // Envelope host/source/sourcetype/index: peek current value; fall back
1144        // to sticky extractor state.
1145        let fields: &[(&str, &str)] = &[
1146            ("host", "splunk_hec.host"),
1147            ("source", "splunk_hec.source"),
1148            ("sourcetype", "splunk_hec.sourcetype"),
1149            ("index", "splunk_hec.index"),
1150        ];
1151        for (json_key, meta_path) in fields {
1152            let val = json
1153                .get(json_key)
1154                .and_then(|v| v.as_str())
1155                .map(|s| Value::from(s.to_string()))
1156                .or_else(|| {
1157                    self.extractors
1158                        .iter()
1159                        .find(|e| e.field == *json_key)
1160                        .and_then(|e| e.value.clone())
1161                });
1162            if let Some(v) = val {
1163                metadata.value_mut().insert(
1164                    &vrl::path::parse_value_path(meta_path)
1165                        .expect("hardcoded splunk_hec metadata path is a valid VRL path"),
1166                    v,
1167                );
1168            }
1169        }
1170
1171        // Channel: envelope field or header default
1172        let channel = json
1173            .get("channel")
1174            .and_then(|v| v.as_str())
1175            .map(|s| Value::from(s.to_string()))
1176            .or_else(|| self.channel.clone());
1177        if let Some(ch) = channel {
1178            metadata.value_mut().insert(
1179                &vrl::path::parse_value_path("splunk_hec.channel")
1180                    .expect("splunk_hec.channel is a valid VRL path"),
1181                ch,
1182            );
1183        }
1184
1185        metadata
1186    }
1187
1188    /// Decoded path: extract the envelope's `event` field as bytes (preserving shape),
1189    /// run it through the second-stage decoder, and overlay envelope metadata so that
1190    /// decoder-produced fields win on conflict. Returns the events along with a flag
1191    /// indicating whether the codec hit any errors (so the caller can refuse to ack
1192    /// a request that lost data).
1193    fn build_events_decoded(
1194        &mut self,
1195        mut json: JsonValue,
1196        decoder: Decoder,
1197    ) -> Result<(Vec<Event>, bool), Rejection> {
1198        self.envelopes_processed += 1;
1199        let event = self.validate_event_field(&json)?;
1200        // Strings are passed as raw bytes so decoders see the bare content
1201        // (e.g. a JSON string event containing `{"foo":"bar"}` arrives at the
1202        // decoder as `{"foo":"bar"}`, not `"{\"foo\":\"bar\"}"` ). All other
1203        // JSON values (objects, arrays, numbers, bools) are serialized to JSON.
1204        let payload = if let Some(s) = event.as_str() {
1205            s.as_bytes().to_vec()
1206        } else {
1207            match serde_json::to_vec(event) {
1208                Ok(bytes) => bytes,
1209                Err(error) => {
1210                    let error: vector_lib::Error = Box::new(error);
1211                    emit!(
1212                        vector_lib::codecs::internal_events::DecoderDeserializeError {
1213                            error: &error
1214                        }
1215                    );
1216                    emit!(ComponentEventsDropped::<UNINTENTIONAL> {
1217                        count: 1,
1218                        reason: "Failed to serialize event field to bytes.",
1219                    });
1220                    return Ok((vec![], true));
1221                }
1222            }
1223        };
1224
1225        self.process_time(&mut json)?;
1226
1227        // Always forward a fallback timestamp so events without an explicit envelope
1228        // `time` field still get one (matches the legacy /event behavior, which always
1229        // wrote a timestamp). `decode_message` uses `try_insert`, so a decoder-supplied
1230        // timestamp still wins on conflict.
1231        let fallback_time = match self.time {
1232            Time::Provided(t) | Time::Now(t) => t,
1233        };
1234
1235        // Build a metadata template so VRL decoders can read envelope context
1236        // via `%`-prefixed paths (e.g. `%splunk_hec.host`, `%vector.secrets.*`).
1237        // For non-VRL decoders `with_metadata_template` is a no-op.
1238        let decoder = decoder.with_metadata_template(self.build_vrl_metadata(&json));
1239
1240        let (decoded, had_decode_errors) = decode_payload(
1241            decoder,
1242            &payload,
1243            Some(fallback_time),
1244            true, // /event: write %splunk_hec.timestamp
1245            DecodePayloadContext {
1246                batch: &self.batch,
1247                log_namespace: self.log_namespace,
1248                events_received: &self.events_received,
1249                splunk_hec_token: self.token.as_ref(),
1250            },
1251        );
1252
1253        // Snapshot envelope metadata that has to apply uniformly to every decoded event.
1254        let envelope_channel: Option<Value> = match json.get_mut("channel").map(JsonValue::take) {
1255            Some(JsonValue::String(guid)) => Some(guid.into()),
1256            _ => None,
1257        };
1258        let envelope_fields: Option<serde_json::Map<String, JsonValue>> =
1259            match json.get_mut("fields").map(JsonValue::take) {
1260                Some(JsonValue::Object(object)) => Some(object),
1261                _ => None,
1262            };
1263        let channel_path = owned_value_path!(CHANNEL);
1264
1265        let mut out = Vec::with_capacity(decoded.len());
1266        for mut event in decoded {
1267            if let Event::Log(log) = &mut event {
1268                // channel: envelope value beats header default. Use `InsertIfEmpty`
1269                // for legacy event fields, and `try_insert` for the Vector metadata
1270                // path so a decoder-produced `%splunk_hec.channel` survives.
1271                if let Some(channel_val) = envelope_channel.clone().or_else(|| self.channel.clone())
1272                {
1273                    match self.log_namespace {
1274                        LogNamespace::Legacy => {
1275                            self.log_namespace.insert_source_metadata(
1276                                SplunkConfig::NAME,
1277                                log,
1278                                Some(LegacyKey::InsertIfEmpty(&channel_path)),
1279                                lookup::path!(CHANNEL),
1280                                channel_val,
1281                            );
1282                        }
1283                        LogNamespace::Vector => {
1284                            log.try_insert(
1285                                metadata_path!(SplunkConfig::NAME, CHANNEL),
1286                                channel_val,
1287                            );
1288                        }
1289                    }
1290                }
1291
1292                // Top-level envelope fields (host/index/source/sourcetype) must be
1293                // applied before `fields.*` so top-level beats `fields.*` when both
1294                // are present — matching the non-decoder runtime precedence. Both
1295                // still use InsertIfEmpty so the decoder's output wins over all
1296                // envelope metadata. Order: decoder > top-level > fields.
1297                for de in self.extractors.iter_mut() {
1298                    de.extract(log, &mut json);
1299                }
1300
1301                // fields: use `InsertIfEmpty` / `try_insert` to preserve decoder-wins
1302                // and extractor-wins semantics (fields fill only what neither the
1303                // decoder nor the top-level envelope keys have already set).
1304                if let Some(ref fields) = envelope_fields {
1305                    for (key, value) in fields {
1306                        match self.log_namespace {
1307                            LogNamespace::Legacy => {
1308                                self.log_namespace.insert_source_metadata(
1309                                    SplunkConfig::NAME,
1310                                    log,
1311                                    Some(LegacyKey::InsertIfEmpty(&owned_value_path!(
1312                                        key.as_str()
1313                                    ))),
1314                                    lookup::path!(key.as_str()),
1315                                    value.clone(),
1316                                );
1317                            }
1318                            LogNamespace::Vector => {
1319                                log.try_insert(
1320                                    metadata_path!(SplunkConfig::NAME, key.as_str()),
1321                                    value.clone(),
1322                                );
1323                            }
1324                        }
1325                    }
1326                }
1327            }
1328            // `splunk_hec_token` is set inside `decode_payload` so the metadata is
1329            // attached at the moment each event leaves the codec. Don't overwrite it
1330            // here.
1331            out.push(event);
1332        }
1333
1334        Ok((out, had_decode_errors))
1335    }
1336
1337    /// Validate the `event` field of a HEC envelope, returning a reference to the
1338    /// validated value or an error if it is missing, null, or (for string values)
1339    /// empty. Shared between the decoder path and the legacy/vector construction
1340    /// paths so they all enforce the same HEC protocol contract.
1341    fn validate_event_field<'a>(&self, json: &'a JsonValue) -> Result<&'a JsonValue, Rejection> {
1342        let event_idx = self.envelopes_processed.saturating_sub(1);
1343        match json.get("event") {
1344            None | Some(JsonValue::Null) => {
1345                Err(ApiError::MissingEventField { event: event_idx }.into())
1346            }
1347            Some(JsonValue::String(s)) if s.is_empty() => {
1348                Err(ApiError::EmptyEventField { event: event_idx }.into())
1349            }
1350            Some(event) => Ok(event),
1351        }
1352    }
1353
1354    /// Build the log event for the vector namespace.
1355    /// In this namespace the log event is created entirely from the event field.
1356    /// No renaming of the `line` field is done.
1357    fn build_log_vector(&mut self, json: &mut JsonValue) -> Result<LogEvent, Rejection> {
1358        let event: Value = self.validate_event_field(json)?.into();
1359        let mut log = LogEvent::from(event);
1360
1361        // EstimatedJsonSizeOf must be calculated before enrichment
1362        self.events_received
1363            .emit(CountByteSize(1, log.estimated_json_encoded_size_of()));
1364
1365        // The timestamp is extracted from the message for the Legacy namespace.
1366        self.log_namespace.insert_vector_metadata(
1367            &mut log,
1368            log_schema().timestamp_key(),
1369            lookup::path!("ingest_timestamp"),
1370            chrono::Utc::now(),
1371        );
1372
1373        Ok(log)
1374    }
1375
1376    /// Build the log event for the legacy namespace.
1377    /// If the event is a string, or the event contains a field called `line` that is a string
1378    /// (the docker splunk logger places the message in the event.line field) that string
1379    /// is placed in the message field.
1380    fn build_log_legacy(&mut self, json: &mut JsonValue) -> Result<LogEvent, Rejection> {
1381        // validate_event_field checks for missing/null/empty-string
1382        self.validate_event_field(json)?;
1383        let mut log = LogEvent::default();
1384        match json["event"].take() {
1385            JsonValue::String(string) => {
1386                log.maybe_insert(log_schema().message_key_target_path(), string);
1387            }
1388            JsonValue::Object(mut object) => {
1389                if object.is_empty() {
1390                    return Err(ApiError::EmptyEventField {
1391                        event: self.envelopes_processed.saturating_sub(1),
1392                    }
1393                    .into());
1394                }
1395
1396                // Add 'line' value as 'event::schema().message_key'
1397                if let Some(line) = object.remove("line") {
1398                    match line {
1399                        // This don't quite fit the meaning of a event::schema().message_key
1400                        JsonValue::Array(_) | JsonValue::Object(_) => {
1401                            log.insert(event_path!("line"), line);
1402                        }
1403                        _ => {
1404                            log.maybe_insert(log_schema().message_key_target_path(), line);
1405                        }
1406                    }
1407                }
1408
1409                for (key, value) in object {
1410                    log.insert(event_path!(key.as_str()), value);
1411                }
1412            }
1413            _ => {
1414                return Err(ApiError::InvalidDataFormat {
1415                    event: self.envelopes_processed.saturating_sub(1),
1416                }
1417                .into());
1418            }
1419        }
1420
1421        // EstimatedJsonSizeOf must be calculated before enrichment
1422        self.events_received
1423            .emit(CountByteSize(1, log.estimated_json_encoded_size_of()));
1424
1425        Ok(log)
1426    }
1427}
1428
1429impl<'de, R: JsonRead<'de>> Iterator for EventIterator<'de, R> {
1430    /// Each item is `(events, had_decode_errors)` for one envelope - the boolean is
1431    /// only ever `true` in the decoder path. Callers OR these together across the
1432    /// whole request to decide whether ack registration is safe.
1433    type Item = Result<(Vec<Event>, bool), Rejection>;
1434
1435    fn next(&mut self) -> Option<Self::Item> {
1436        match self.deserializer.next() {
1437            Some(Ok(json)) => {
1438                let result = if let Some(decoder) = self.decoder.clone() {
1439                    self.build_events_decoded(json, decoder)
1440                } else {
1441                    self.build_event(json).map(|event| (vec![event], false))
1442                };
1443                Some(result)
1444            }
1445            None => {
1446                if self.envelopes_processed == 0 {
1447                    Some(Err(ApiError::NoData.into()))
1448                } else {
1449                    None
1450                }
1451            }
1452            Some(Err(error)) => {
1453                emit!(SplunkHecRequestBodyInvalidError {
1454                    error: error.into()
1455                });
1456                // The deserializer failed to parse the next envelope, so the failing
1457                // envelope's index is the count of envelopes already processed (not
1458                // `envelopes_processed - 1`, which is what build-time errors use).
1459                Some(Err(ApiError::InvalidDataFormat {
1460                    event: self.envelopes_processed,
1461                }
1462                .into()))
1463            }
1464        }
1465    }
1466}
1467
1468struct DecodePayloadContext<'a> {
1469    batch: &'a Option<BatchNotifier>,
1470    log_namespace: LogNamespace,
1471    events_received: &'a Registered<EventsReceived>,
1472    splunk_hec_token: Option<&'a Arc<str>>,
1473}
1474
1475/// Run a payload through the configured `framing` + `decoding` codec.
1476///
1477/// Returns the decoded events along with a flag indicating whether any decode error
1478/// occurred. The shared `crate::sources::util::decode_message` helper swallows
1479/// decode errors silently, which is fine for sources without ack semantics, but for
1480/// `splunk_hec` we need to know about errors so we can refuse to acknowledge a
1481/// request that lost data mid-stream.
1482///
1483/// On each decoded event this helper sets `source_type`, `vector.ingest_timestamp`,
1484/// the optional `splunk_hec.timestamp` (only when `set_source_timestamp` is `true`,
1485/// i.e. for the `/event` endpoint which carries an HEC envelope `time` field), and
1486/// the optional Splunk HEC token. Pass `set_source_timestamp = false` for `/raw`,
1487/// which has no envelope timestamp and should only receive `%vector.ingest_timestamp`.
1488fn decode_payload(
1489    mut decoder: Decoder,
1490    payload: &[u8],
1491    fallback_timestamp: Option<DateTime<Utc>>,
1492    set_source_timestamp: bool,
1493    ctx: DecodePayloadContext<'_>,
1494) -> (Vec<Event>, bool) {
1495    let DecodePayloadContext {
1496        batch,
1497        log_namespace,
1498        events_received,
1499        splunk_hec_token,
1500    } = ctx;
1501    let mut buffer = BytesMut::with_capacity(payload.len());
1502    buffer.extend_from_slice(payload);
1503    let now = Utc::now();
1504    let mut events: Vec<Event> = Vec::new();
1505    let mut had_errors = false;
1506
1507    loop {
1508        match decoder.decode_eof(&mut buffer) {
1509            Ok(Some((decoded, _))) => {
1510                for mut event in decoded {
1511                    if let Event::Log(log) = &mut event {
1512                        log_namespace.insert_vector_metadata(
1513                            log,
1514                            log_schema().source_type_key(),
1515                            lookup::path!("source_type"),
1516                            Bytes::from_static(SplunkConfig::NAME.as_bytes()),
1517                        );
1518                        match log_namespace {
1519                            LogNamespace::Vector => {
1520                                // Only write %splunk_hec.timestamp for the /event
1521                                // endpoint, which has a real HEC envelope timestamp.
1522                                // /raw has no envelope time and should only get the
1523                                // standard %vector.ingest_timestamp below.
1524                                if set_source_timestamp && let Some(timestamp) = fallback_timestamp
1525                                {
1526                                    log.try_insert(
1527                                        metadata_path!(SplunkConfig::NAME, "timestamp"),
1528                                        timestamp,
1529                                    );
1530                                }
1531                                log.insert(metadata_path!("vector", "ingest_timestamp"), now);
1532                            }
1533                            LogNamespace::Legacy => {
1534                                if let Some(timestamp) = fallback_timestamp
1535                                    && let Some(timestamp_key) = log_schema().timestamp_key()
1536                                {
1537                                    log.try_insert((PathPrefix::Event, timestamp_key), timestamp);
1538                                }
1539                            }
1540                        }
1541                    }
1542                    if let Some(token) = splunk_hec_token {
1543                        event.metadata_mut().set_splunk_hec_token(Arc::clone(token));
1544                    }
1545                    events_received.emit(CountByteSize(1, event.estimated_json_encoded_size_of()));
1546                    events.push(event.with_batch_notifier_option(batch));
1547                }
1548            }
1549            Ok(None) => break,
1550            Err(error) => {
1551                // The decoder logs its own error; record that one occurred so the
1552                // caller can refuse to ack a request that lost data.
1553                had_errors = true;
1554                if !error.can_continue() {
1555                    break;
1556                }
1557            }
1558        }
1559    }
1560
1561    (events, had_errors)
1562}
1563
1564/// Parse a `i64` unix timestamp that can either be in seconds, milliseconds or
1565/// nanoseconds.
1566///
1567/// This attempts to parse timestamps based on what cutoff range they fall into.
1568/// For seconds to be parsed the timestamp must be less than the unix epoch of
1569/// the year `2400`. For this to parse milliseconds the time must be smaller
1570/// than the year `10,000` in unix epoch milliseconds. If the value is larger
1571/// than both we attempt to parse it as nanoseconds.
1572///
1573/// Returns `None` if `t` is negative.
1574fn parse_timestamp(t: i64) -> Option<DateTime<Utc>> {
1575    // Utc.ymd(2400, 1, 1).and_hms(0,0,0).timestamp();
1576    const SEC_CUTOFF: i64 = 13569465600;
1577    // Utc.ymd(10_000, 1, 1).and_hms(0,0,0).timestamp_millis();
1578    const MILLISEC_CUTOFF: i64 = 253402300800000;
1579
1580    // Timestamps can't be negative!
1581    if t < 0 {
1582        return None;
1583    }
1584
1585    let ts = if t < SEC_CUTOFF {
1586        Utc.timestamp_opt(t, 0).single().expect("invalid timestamp")
1587    } else if t < MILLISEC_CUTOFF {
1588        Utc.timestamp_millis_opt(t)
1589            .single()
1590            .expect("invalid timestamp")
1591    } else {
1592        Utc.timestamp_nanos(t)
1593    };
1594
1595    Some(ts)
1596}
1597
1598/// How to write the legacy key when `DefaultExtractor::extract` applies a value.
1599#[derive(Clone, Copy)]
1600enum LegacyKeyStrategy {
1601    Overwrite,
1602    InsertIfEmpty,
1603}
1604
1605/// Maintains last known extracted value of field and uses it in the absence of field.
1606struct DefaultExtractor {
1607    field: &'static str,
1608    to_field: OptionalValuePath,
1609    value: Option<Value>,
1610    log_namespace: LogNamespace,
1611    legacy_key_strategy: LegacyKeyStrategy,
1612}
1613
1614impl DefaultExtractor {
1615    const fn new(
1616        field: &'static str,
1617        to_field: OptionalValuePath,
1618        log_namespace: LogNamespace,
1619    ) -> Self {
1620        DefaultExtractor {
1621            field,
1622            to_field,
1623            value: None,
1624            log_namespace,
1625            legacy_key_strategy: LegacyKeyStrategy::Overwrite,
1626        }
1627    }
1628
1629    fn new_with(
1630        field: &'static str,
1631        to_field: OptionalValuePath,
1632        value: impl Into<Option<Value>>,
1633        log_namespace: LogNamespace,
1634    ) -> Self {
1635        DefaultExtractor {
1636            field,
1637            to_field,
1638            value: value.into(),
1639            log_namespace,
1640            legacy_key_strategy: LegacyKeyStrategy::Overwrite,
1641        }
1642    }
1643
1644    /// Set the strategy used when writing this extractor's legacy key. Defaults to
1645    /// `Overwrite`; the decoder path uses `InsertIfEmpty` for fields that may collide
1646    /// with decoder-produced output (e.g. `host`).
1647    const fn with_legacy_key_strategy(mut self, strategy: LegacyKeyStrategy) -> Self {
1648        self.legacy_key_strategy = strategy;
1649        self
1650    }
1651
1652    fn extract(&mut self, log: &mut LogEvent, value: &mut JsonValue) {
1653        // Process json_field
1654        if let Some(JsonValue::String(new_value)) = value.get_mut(self.field).map(JsonValue::take) {
1655            self.value = Some(new_value.into());
1656        }
1657
1658        // Add data field
1659        if let Some(index) = self.value.as_ref()
1660            && let Some(metadata_key) = self.to_field.path.as_ref()
1661        {
1662            // For Vector namespace + InsertIfEmpty (decoder mode): check the metadata
1663            // value tree before inserting so VRL-produced values aren't overwritten.
1664            // `insert_source_metadata` for Vector ns always calls `insert`, not
1665            // `try_insert`, so we replicate its path construction here.
1666            if matches!(self.log_namespace, LogNamespace::Vector)
1667                && matches!(self.legacy_key_strategy, LegacyKeyStrategy::InsertIfEmpty)
1668            {
1669                log.try_insert(
1670                    (
1671                        PathPrefix::Metadata,
1672                        lookup::path!(SplunkConfig::NAME).concat(metadata_key),
1673                    ),
1674                    index.clone(),
1675                );
1676            } else {
1677                let legacy_key = match self.legacy_key_strategy {
1678                    LegacyKeyStrategy::Overwrite => LegacyKey::Overwrite(metadata_key),
1679                    LegacyKeyStrategy::InsertIfEmpty => LegacyKey::InsertIfEmpty(metadata_key),
1680                };
1681                self.log_namespace.insert_source_metadata(
1682                    SplunkConfig::NAME,
1683                    log,
1684                    Some(legacy_key),
1685                    &self.to_field.path.clone().unwrap_or(owned_value_path!("")),
1686                    index.clone(),
1687                );
1688            }
1689        }
1690    }
1691}
1692
1693/// For tracking origin of the timestamp
1694#[derive(Clone, Debug)]
1695enum Time {
1696    /// Backup
1697    Now(DateTime<Utc>),
1698    /// Provided in the request
1699    Provided(DateTime<Utc>),
1700}
1701
1702/// Creates events from a raw HEC request body.
1703///
1704/// Without a decoder, returns a single event whose message is the (decompressed)
1705/// request body. With a decoder, the body is fed through the configured framing +
1706/// decoding pipeline and one or more events are returned. The boolean second tuple
1707/// element is `true` when the decoder hit any (recoverable or non-recoverable)
1708/// errors during the request, so the caller can refuse to acknowledge the request.
1709#[allow(clippy::too_many_arguments)]
1710fn raw_event(
1711    bytes: Bytes,
1712    gzip: bool,
1713    channel: String,
1714    remote: Option<SocketAddr>,
1715    xff: Option<String>,
1716    batch: Option<BatchNotifier>,
1717    log_namespace: LogNamespace,
1718    events_received: &Registered<EventsReceived>,
1719    decoder: Option<Decoder>,
1720    splunk_hec_token: Option<Arc<str>>,
1721) -> Result<(Vec<Event>, bool), Rejection> {
1722    // Process gzip
1723    let body_bytes: Bytes = if gzip {
1724        // Cap the decompressed output to mitigate gzip-bomb DoS.
1725        match CappedDecoder::gzip(bytes.reader()).decompress() {
1726            Ok(data) if data.is_empty() => return Err(ApiError::NoData.into()),
1727            Ok(data) => Bytes::from(data),
1728            Err(error) => {
1729                emit!(SplunkHecRequestBodyInvalidError { error });
1730                return Err(ApiError::InvalidDataFormat { event: 0 }.into());
1731            }
1732        }
1733    } else {
1734        bytes
1735    };
1736
1737    // host-field priority for raw endpoint:
1738    // - x-forwarded-for is set to `host` field first, if present. If not present:
1739    // - set remote addr to host field
1740    let host = if let Some(remote_address) = xff {
1741        Some(remote_address)
1742    } else {
1743        remote.map(|remote| remote.to_string())
1744    };
1745
1746    let decoder_in_use = decoder.is_some();
1747    let (mut events, had_decode_errors): (Vec<Event>, bool) = if let Some(decoder) = decoder {
1748        // Build a metadata template so VRL decoders can read raw-endpoint context
1749        // via `%`-prefixed paths (e.g. `%splunk_hec.channel`, `%splunk_hec.host`,
1750        // `%vector.secrets.splunk_hec_token`). No-op for non-VRL decoders.
1751        let decoder = {
1752            let mut meta = EventMetadata::default();
1753            if let Some(token) = splunk_hec_token.as_ref() {
1754                meta.set_splunk_hec_token(Arc::clone(token));
1755            }
1756            if let Some(ref h) = host {
1757                meta.value_mut().insert(
1758                    &vrl::path::parse_value_path("splunk_hec.host")
1759                        .expect("splunk_hec.host is a valid VRL path"),
1760                    h.clone(),
1761                );
1762            }
1763            meta.value_mut().insert(
1764                &vrl::path::parse_value_path("splunk_hec.channel")
1765                    .expect("splunk_hec.channel is a valid VRL path"),
1766                channel.clone(),
1767            );
1768            decoder.with_metadata_template(meta)
1769        };
1770
1771        // Pass ingest time as the fallback timestamp so decoded events always have
1772        // one - matches `insert_standard_vector_source_metadata` in the legacy raw
1773        // path. `decode_payload` uses `try_insert`, so a decoder-supplied timestamp
1774        // still wins on conflict.
1775        decode_payload(
1776            decoder,
1777            &body_bytes,
1778            Some(Utc::now()),
1779            false, // /raw: no HEC envelope timestamp; only %vector.ingest_timestamp
1780            DecodePayloadContext {
1781                batch: &batch,
1782                log_namespace,
1783                events_received,
1784                splunk_hec_token: splunk_hec_token.as_ref(),
1785            },
1786        )
1787    } else {
1788        let message: Value = body_bytes.into();
1789        let mut log = match log_namespace {
1790            LogNamespace::Vector => LogEvent::from(message),
1791            LogNamespace::Legacy => {
1792                let mut log = LogEvent::default();
1793                log.maybe_insert(log_schema().message_key_target_path(), message);
1794                log
1795            }
1796        };
1797        // We need to calculate the estimated json size of the event BEFORE enrichment.
1798        events_received.emit(CountByteSize(1, log.estimated_json_encoded_size_of()));
1799
1800        log_namespace.insert_standard_vector_source_metadata(
1801            &mut log,
1802            SplunkConfig::NAME,
1803            Utc::now(),
1804        );
1805
1806        if let Some(batch) = batch.clone() {
1807            log = log.with_batch_notifier(&batch);
1808        }
1809        (vec![Event::from(log)], false)
1810    };
1811
1812    let channel_path = owned_value_path!(CHANNEL);
1813    for event in &mut events {
1814        if let Event::Log(log) = event {
1815            // With a decoder configured, defer to anything it produced at the legacy
1816            // When a decoder is in use, preserve decoder-wins semantics for Vector ns
1817            // by using `try_insert` on the metadata path (insert_source_metadata for
1818            // Vector ns always overwrites). Without a decoder the log is freshly
1819            // constructed so overwriting is correct.
1820            if decoder_in_use && matches!(log_namespace, LogNamespace::Vector) {
1821                log.try_insert(metadata_path!(SplunkConfig::NAME, CHANNEL), channel.clone());
1822                if let Some(ref h) = host {
1823                    log.try_insert(metadata_path!(SplunkConfig::NAME, "host"), h.clone());
1824                }
1825            } else {
1826                let channel_legacy_key = if decoder_in_use {
1827                    LegacyKey::InsertIfEmpty(&channel_path)
1828                } else {
1829                    LegacyKey::Overwrite(&channel_path)
1830                };
1831                log_namespace.insert_source_metadata(
1832                    SplunkConfig::NAME,
1833                    log,
1834                    Some(channel_legacy_key),
1835                    lookup::path!(CHANNEL),
1836                    channel.clone(),
1837                );
1838                if let Some(ref host) = host {
1839                    log_namespace.insert_source_metadata(
1840                        SplunkConfig::NAME,
1841                        log,
1842                        log_schema().host_key().map(LegacyKey::InsertIfEmpty),
1843                        lookup::path!("host"),
1844                        host.clone(),
1845                    );
1846                }
1847            }
1848        }
1849    }
1850
1851    Ok((events, had_decode_errors))
1852}
1853
1854#[derive(Clone, Copy, Debug, Snafu)]
1855pub(crate) enum ApiError {
1856    MissingAuthorization,
1857    InvalidAuthorization,
1858    UnsupportedEncoding,
1859    UnsupportedContentType,
1860    MissingChannel,
1861    NoData,
1862    InvalidDataFormat { event: usize },
1863    ServerShutdown,
1864    EmptyEventField { event: usize },
1865    MissingEventField { event: usize },
1866    BadRequest,
1867    ServiceUnavailable,
1868    AckIsDisabled,
1869}
1870
1871impl warp::reject::Reject for ApiError {}
1872
1873/// Cached bodies for common responses
1874mod splunk_response {
1875    use serde::Serialize;
1876
1877    // https://docs.splunk.com/Documentation/Splunk/8.2.3/Data/TroubleshootHTTPEventCollector#Possible_error_codes
1878    pub enum HecStatusCode {
1879        Success = 0,
1880        TokenIsRequired = 2,
1881        InvalidAuthorization = 3,
1882        NoData = 5,
1883        InvalidDataFormat = 6,
1884        ServerIsBusy = 9,
1885        DataChannelIsMissing = 10,
1886        EventFieldIsRequired = 12,
1887        EventFieldCannotBeBlank = 13,
1888        AckIsDisabled = 14,
1889    }
1890
1891    #[derive(Serialize)]
1892    pub enum HecResponseMetadata {
1893        #[serde(rename = "ackId")]
1894        AckId(u64),
1895        #[serde(rename = "invalid-event-number")]
1896        InvalidEventNumber(usize),
1897    }
1898
1899    #[derive(Serialize)]
1900    pub struct HecResponse {
1901        text: &'static str,
1902        code: u8,
1903        #[serde(skip_serializing_if = "Option::is_none", flatten)]
1904        pub metadata: Option<HecResponseMetadata>,
1905    }
1906
1907    impl HecResponse {
1908        pub const fn new(code: HecStatusCode) -> Self {
1909            let text = match code {
1910                HecStatusCode::Success => "Success",
1911                HecStatusCode::TokenIsRequired => "Token is required",
1912                HecStatusCode::InvalidAuthorization => "Invalid authorization",
1913                HecStatusCode::NoData => "No data",
1914                HecStatusCode::InvalidDataFormat => "Invalid data format",
1915                HecStatusCode::DataChannelIsMissing => "Data channel is missing",
1916                HecStatusCode::EventFieldIsRequired => "Event field is required",
1917                HecStatusCode::EventFieldCannotBeBlank => "Event field cannot be blank",
1918                HecStatusCode::ServerIsBusy => "Server is busy",
1919                HecStatusCode::AckIsDisabled => "Ack is disabled",
1920            };
1921
1922            Self {
1923                text,
1924                code: code as u8,
1925                metadata: None,
1926            }
1927        }
1928
1929        pub const fn with_metadata(mut self, metadata: HecResponseMetadata) -> Self {
1930            self.metadata = Some(metadata);
1931            self
1932        }
1933    }
1934
1935    pub const INVALID_AUTHORIZATION: HecResponse =
1936        HecResponse::new(HecStatusCode::InvalidAuthorization);
1937    pub const TOKEN_IS_REQUIRED: HecResponse = HecResponse::new(HecStatusCode::TokenIsRequired);
1938    pub const NO_DATA: HecResponse = HecResponse::new(HecStatusCode::NoData);
1939    pub const SUCCESS: HecResponse = HecResponse::new(HecStatusCode::Success);
1940    pub const SERVER_IS_BUSY: HecResponse = HecResponse::new(HecStatusCode::ServerIsBusy);
1941    pub const NO_CHANNEL: HecResponse = HecResponse::new(HecStatusCode::DataChannelIsMissing);
1942    pub const ACK_IS_DISABLED: HecResponse = HecResponse::new(HecStatusCode::AckIsDisabled);
1943}
1944
1945async fn register_ack(
1946    idx_ack: Option<Arc<IndexerAcknowledgement>>,
1947    receiver: Option<BatchStatusReceiver>,
1948    channel: Option<String>,
1949) -> Result<Option<u64>, Rejection> {
1950    match (idx_ack, receiver, channel) {
1951        (Some(ack), Some(rx), Some(ch)) => Ok(Some(ack.get_ack_id_from_channel(ch, rx).await?)),
1952        _ => Ok(None),
1953    }
1954}
1955
1956fn finish_ok(maybe_ack_id: Option<u64>) -> Response {
1957    let body = if let Some(ack_id) = maybe_ack_id {
1958        HecResponse::new(HecStatusCode::Success).with_metadata(HecResponseMetadata::AckId(ack_id))
1959    } else {
1960        splunk_response::SUCCESS
1961    };
1962    response_json(StatusCode::OK, body)
1963}
1964
1965fn response_plain(code: StatusCode, msg: &'static str) -> Response {
1966    warp::reply::with_status(
1967        warp::reply::with_header(msg, http::header::CONTENT_TYPE, "text/plain; charset=utf-8"),
1968        code,
1969    )
1970    .into_response()
1971}
1972
1973async fn finish_err(rejection: Rejection) -> Result<(Response,), Rejection> {
1974    if let Some(&error) = rejection.find::<ApiError>() {
1975        emit!(SplunkHecRequestError { error });
1976        Ok((match error {
1977            ApiError::MissingAuthorization => {
1978                response_json(StatusCode::UNAUTHORIZED, splunk_response::TOKEN_IS_REQUIRED)
1979            }
1980            ApiError::InvalidAuthorization => response_json(
1981                StatusCode::UNAUTHORIZED,
1982                splunk_response::INVALID_AUTHORIZATION,
1983            ),
1984            ApiError::UnsupportedEncoding => empty_response(StatusCode::UNSUPPORTED_MEDIA_TYPE),
1985            ApiError::UnsupportedContentType => response_plain(
1986                StatusCode::UNSUPPORTED_MEDIA_TYPE,
1987                "The request's content-type is not supported",
1988            ),
1989            ApiError::MissingChannel => {
1990                response_json(StatusCode::BAD_REQUEST, splunk_response::NO_CHANNEL)
1991            }
1992            ApiError::NoData => response_json(StatusCode::BAD_REQUEST, splunk_response::NO_DATA),
1993            ApiError::ServerShutdown => empty_response(StatusCode::SERVICE_UNAVAILABLE),
1994            ApiError::InvalidDataFormat { event } => response_json(
1995                StatusCode::BAD_REQUEST,
1996                HecResponse::new(HecStatusCode::InvalidDataFormat)
1997                    .with_metadata(HecResponseMetadata::InvalidEventNumber(event)),
1998            ),
1999            ApiError::EmptyEventField { event } => response_json(
2000                StatusCode::BAD_REQUEST,
2001                HecResponse::new(HecStatusCode::EventFieldCannotBeBlank)
2002                    .with_metadata(HecResponseMetadata::InvalidEventNumber(event)),
2003            ),
2004            ApiError::MissingEventField { event } => response_json(
2005                StatusCode::BAD_REQUEST,
2006                HecResponse::new(HecStatusCode::EventFieldIsRequired)
2007                    .with_metadata(HecResponseMetadata::InvalidEventNumber(event)),
2008            ),
2009            ApiError::BadRequest => empty_response(StatusCode::BAD_REQUEST),
2010            ApiError::ServiceUnavailable => response_json(
2011                StatusCode::SERVICE_UNAVAILABLE,
2012                splunk_response::SERVER_IS_BUSY,
2013            ),
2014            ApiError::AckIsDisabled => {
2015                response_json(StatusCode::BAD_REQUEST, splunk_response::ACK_IS_DISABLED)
2016            }
2017        },))
2018    } else if let Some(error) = rejection.find::<ErrorMessage>() {
2019        Ok((response_json(error.status_code(), error),))
2020    } else {
2021        Err(rejection)
2022    }
2023}
2024
2025/// Response without body
2026fn empty_response(code: StatusCode) -> Response {
2027    let mut res = Response::default();
2028    *res.status_mut() = code;
2029    res
2030}
2031
2032/// Response with body
2033fn response_json(code: StatusCode, body: impl Serialize) -> Response {
2034    warp::reply::with_status(warp::reply::json(&body), code).into_response()
2035}
2036
2037#[cfg(feature = "sinks-splunk_hec")]
2038#[cfg(test)]
2039mod tests {
2040    use std::{net::SocketAddr, num::NonZeroU64};
2041
2042    use chrono::{TimeZone, Utc};
2043    use futures_util::Stream;
2044    use http::Uri;
2045    use reqwest::{RequestBuilder, Response};
2046    use serde::Deserialize;
2047    use vector_lib::{
2048        codecs::{
2049            BytesDecoderConfig, JsonSerializerConfig, TextSerializerConfig,
2050            decoding::{
2051                DeserializerConfig,
2052                format::{VrlDeserializerConfig, VrlDeserializerOptions},
2053            },
2054        },
2055        event::EventStatus,
2056        schema::Definition,
2057        sensitive_string::SensitiveString,
2058    };
2059    use vrl::path::PathPrefix;
2060
2061    use super::*;
2062    use crate::{
2063        SourceSender,
2064        codecs::{DecodingConfig, EncodingConfig},
2065        components::validation::prelude::*,
2066        config::{SinkConfig, SinkContext, SourceConfig, SourceContext, log_schema},
2067        event::{Event, LogEvent},
2068        sinks::{
2069            Healthcheck, VectorSink,
2070            splunk_hec::logs::config::HecLogsSinkConfig,
2071            util::{BatchConfig, Compression, TowerRequestConfig},
2072        },
2073        sources::splunk_hec::acknowledgements::{HecAckStatusRequest, HecAckStatusResponse},
2074        test_util::{
2075            addr::{PortGuard, next_addr},
2076            collect_n,
2077            components::{
2078                COMPONENT_ERROR_TAGS, HTTP_PUSH_SOURCE_TAGS, assert_source_compliance,
2079                assert_source_error,
2080            },
2081            wait_for_tcp,
2082        },
2083    };
2084
2085    #[test]
2086    fn generate_config() {
2087        crate::test_util::test_generate_config::<SplunkConfig>();
2088    }
2089
2090    #[tokio::test]
2091    async fn finish_err_maps_capped_body_to_client_error() {
2092        // `capped_body()` rejects oversized payloads with an `ErrorMessage` (e.g. 413). The
2093        // recovery must surface that status instead of letting it fall through to a 500.
2094        let rejection = warp::reject::custom(ErrorMessage::new(
2095            StatusCode::PAYLOAD_TOO_LARGE,
2096            "Request body exceeds limit of 1024 bytes.".to_owned(),
2097        ));
2098
2099        let (response,) = finish_err(rejection)
2100            .await
2101            .expect("capped-body rejection should be recovered");
2102
2103        assert_eq!(response.status(), StatusCode::PAYLOAD_TOO_LARGE);
2104    }
2105
2106    /// Splunk token
2107    const TOKEN: &str = "token";
2108    const VALID_TOKENS: &[&str; 2] = &[TOKEN, "secondary-token"];
2109
2110    async fn source(
2111        acknowledgements: Option<HecAcknowledgementsConfig>,
2112    ) -> (impl Stream<Item = Event> + Unpin, SocketAddr, PortGuard) {
2113        source_with(Some(TOKEN.to_owned().into()), None, acknowledgements, false).await
2114    }
2115
2116    async fn source_with(
2117        token: Option<SensitiveString>,
2118        valid_tokens: Option<&[&str]>,
2119        acknowledgements: Option<HecAcknowledgementsConfig>,
2120        store_hec_token: bool,
2121    ) -> (
2122        impl Stream<Item = Event> + Unpin + use<>,
2123        SocketAddr,
2124        PortGuard,
2125    ) {
2126        let (sender, recv) = SourceSender::new_test_finalize(EventStatus::Delivered);
2127        let (_guard, address) = next_addr();
2128        let valid_tokens =
2129            valid_tokens.map(|tokens| tokens.iter().map(|v| v.to_string().into()).collect());
2130        let cx = SourceContext::new_test(sender, None);
2131        tokio::spawn(async move {
2132            SplunkConfig {
2133                address,
2134                token,
2135                valid_tokens,
2136                tls: None,
2137                acknowledgements: acknowledgements.unwrap_or_default(),
2138                store_hec_token,
2139                log_namespace: None,
2140                keepalive: Default::default(),
2141                event: CodecConfig::default(),
2142                raw: CodecConfig::default(),
2143            }
2144            .build(cx)
2145            .await
2146            .unwrap()
2147            .await
2148            .unwrap()
2149        });
2150        wait_for_tcp(address).await;
2151        (recv, address, _guard)
2152    }
2153
2154    async fn sink(
2155        address: SocketAddr,
2156        encoding: EncodingConfig,
2157        compression: Compression,
2158    ) -> (VectorSink, Healthcheck) {
2159        HecLogsSinkConfig {
2160            default_token: TOKEN.to_owned().into(),
2161            endpoint: format!("http://{address}"),
2162            host_key: None,
2163            indexed_fields: vec![],
2164            index: None,
2165            sourcetype: None,
2166            source: None,
2167            encoding,
2168            compression,
2169            batch: BatchConfig::default(),
2170            request: TowerRequestConfig::default(),
2171            tls: None,
2172            acknowledgements: Default::default(),
2173            timestamp_nanos_key: None,
2174            timestamp_key: None,
2175            auto_extract_timestamp: None,
2176            endpoint_target: Default::default(),
2177            confinement: Default::default(),
2178        }
2179        .build(SinkContext::default())
2180        .await
2181        .unwrap()
2182    }
2183
2184    async fn start(
2185        encoding: EncodingConfig,
2186        compression: Compression,
2187        acknowledgements: Option<HecAcknowledgementsConfig>,
2188    ) -> (VectorSink, impl Stream<Item = Event> + Unpin) {
2189        let (source, address, _guard) = source(acknowledgements).await;
2190        let (sink, health) = sink(address, encoding, compression).await;
2191        assert!(health.await.is_ok());
2192        (sink, source)
2193    }
2194
2195    async fn channel_n(
2196        messages: Vec<impl Into<String> + Send + 'static>,
2197        sink: VectorSink,
2198        source: impl Stream<Item = Event> + Unpin,
2199    ) -> Vec<Event> {
2200        let n = messages.len();
2201
2202        tokio::spawn(async move {
2203            sink.run_events(
2204                messages
2205                    .into_iter()
2206                    .map(|s| Event::Log(LogEvent::from(s.into()))),
2207            )
2208            .await
2209            .unwrap();
2210        });
2211
2212        let events = collect_n(source, n).await;
2213        assert_eq!(n, events.len());
2214
2215        events
2216    }
2217
2218    #[derive(Clone, Copy, Debug)]
2219    enum Channel<'a> {
2220        Header(&'a str),
2221        QueryParam(&'a str),
2222    }
2223
2224    #[derive(Default)]
2225    struct SendWithOpts<'a> {
2226        channel: Option<Channel<'a>>,
2227        forwarded_for: Option<String>,
2228    }
2229
2230    async fn post(address: SocketAddr, api: &str, message: &str) -> u16 {
2231        let channel = Channel::Header("channel");
2232        let options = SendWithOpts {
2233            channel: Some(channel),
2234            forwarded_for: None,
2235        };
2236        send_with(address, api, message, TOKEN, &options).await
2237    }
2238
2239    fn build_request(
2240        address: SocketAddr,
2241        api: &str,
2242        message: &str,
2243        token: &str,
2244        opts: &SendWithOpts<'_>,
2245    ) -> RequestBuilder {
2246        let mut b = reqwest::Client::new()
2247            .post(format!("http://{address}/{api}"))
2248            .header("Authorization", format!("Splunk {token}"));
2249
2250        b = match opts.channel {
2251            Some(c) => match c {
2252                Channel::Header(v) => b.header("x-splunk-request-channel", v),
2253                Channel::QueryParam(v) => b.query(&[("channel", v)]),
2254            },
2255            None => b,
2256        };
2257
2258        b = match &opts.forwarded_for {
2259            Some(f) => b.header("X-Forwarded-For", f),
2260            None => b,
2261        };
2262
2263        b.body(message.to_owned())
2264    }
2265
2266    async fn send_with(
2267        address: SocketAddr,
2268        api: &str,
2269        message: &str,
2270        token: &str,
2271        opts: &SendWithOpts<'_>,
2272    ) -> u16 {
2273        let b = build_request(address, api, message, token, opts);
2274        b.send().await.unwrap().status().as_u16()
2275    }
2276
2277    async fn send_with_response(
2278        address: SocketAddr,
2279        api: &str,
2280        message: &str,
2281        token: &str,
2282        opts: &SendWithOpts<'_>,
2283    ) -> Response {
2284        let b = build_request(address, api, message, token, opts);
2285        b.send().await.unwrap()
2286    }
2287
2288    #[tokio::test]
2289    async fn no_compression_text_event() {
2290        let message = "gzip_text_event";
2291        let (sink, source) = start(
2292            TextSerializerConfig::default().into(),
2293            Compression::None,
2294            None,
2295        )
2296        .await;
2297
2298        let event = channel_n(vec![message], sink, source).await.remove(0);
2299
2300        assert_eq!(
2301            event.as_log()[log_schema().message_key().unwrap().to_string()],
2302            message.into()
2303        );
2304        assert!(event.as_log().get_timestamp().is_some());
2305        assert_eq!(
2306            event.as_log()[log_schema().source_type_key().unwrap().to_string()],
2307            "splunk_hec".into()
2308        );
2309        assert!(event.metadata().splunk_hec_token().is_none());
2310    }
2311
2312    #[tokio::test]
2313    async fn one_simple_text_event() {
2314        let message = "one_simple_text_event";
2315        let (sink, source) = start(
2316            TextSerializerConfig::default().into(),
2317            Compression::gzip_default(),
2318            None,
2319        )
2320        .await;
2321
2322        let event = channel_n(vec![message], sink, source).await.remove(0);
2323
2324        assert_eq!(
2325            event.as_log()[log_schema().message_key().unwrap().to_string()],
2326            message.into()
2327        );
2328        assert!(event.as_log().get_timestamp().is_some());
2329        assert_eq!(
2330            event.as_log()[log_schema().source_type_key().unwrap().to_string()],
2331            "splunk_hec".into()
2332        );
2333        assert!(event.metadata().splunk_hec_token().is_none());
2334    }
2335
2336    #[tokio::test]
2337    async fn multiple_simple_text_event() {
2338        let n = 200;
2339        let (sink, source) = start(
2340            TextSerializerConfig::default().into(),
2341            Compression::None,
2342            None,
2343        )
2344        .await;
2345
2346        let messages = (0..n)
2347            .map(|i| format!("multiple_simple_text_event_{i}"))
2348            .collect::<Vec<_>>();
2349        let events = channel_n(messages.clone(), sink, source).await;
2350
2351        for (msg, event) in messages.into_iter().zip(events.into_iter()) {
2352            assert_eq!(
2353                event.as_log()[log_schema().message_key().unwrap().to_string()],
2354                msg.into()
2355            );
2356            assert!(event.as_log().get_timestamp().is_some());
2357            assert_eq!(
2358                event.as_log()[log_schema().source_type_key().unwrap().to_string()],
2359                "splunk_hec".into()
2360            );
2361            assert!(event.metadata().splunk_hec_token().is_none());
2362        }
2363    }
2364
2365    #[tokio::test]
2366    async fn one_simple_json_event() {
2367        let message = "one_simple_json_event";
2368        let (sink, source) = start(
2369            JsonSerializerConfig::default().into(),
2370            Compression::gzip_default(),
2371            None,
2372        )
2373        .await;
2374
2375        let event = channel_n(vec![message], sink, source).await.remove(0);
2376
2377        assert_eq!(
2378            event.as_log()[log_schema().message_key().unwrap().to_string()],
2379            message.into()
2380        );
2381        assert!(event.as_log().get_timestamp().is_some());
2382        assert_eq!(
2383            event.as_log()[log_schema().source_type_key().unwrap().to_string()],
2384            "splunk_hec".into()
2385        );
2386        assert!(event.metadata().splunk_hec_token().is_none());
2387    }
2388
2389    #[tokio::test]
2390    async fn multiple_simple_json_event() {
2391        let n = 200;
2392        let (sink, source) = start(
2393            JsonSerializerConfig::default().into(),
2394            Compression::gzip_default(),
2395            None,
2396        )
2397        .await;
2398
2399        let messages = (0..n)
2400            .map(|i| format!("multiple_simple_json_event{i}"))
2401            .collect::<Vec<_>>();
2402        let events = channel_n(messages.clone(), sink, source).await;
2403
2404        for (msg, event) in messages.into_iter().zip(events.into_iter()) {
2405            assert_eq!(
2406                event.as_log()[log_schema().message_key().unwrap().to_string()],
2407                msg.into()
2408            );
2409            assert!(event.as_log().get_timestamp().is_some());
2410            assert_eq!(
2411                event.as_log()[log_schema().source_type_key().unwrap().to_string()],
2412                "splunk_hec".into()
2413            );
2414            assert!(event.metadata().splunk_hec_token().is_none());
2415        }
2416    }
2417
2418    #[tokio::test]
2419    async fn json_event() {
2420        let (sink, source) = start(
2421            JsonSerializerConfig::default().into(),
2422            Compression::gzip_default(),
2423            None,
2424        )
2425        .await;
2426
2427        let mut log = LogEvent::default();
2428        log.insert(event_path!("greeting"), "hello");
2429        log.insert(event_path!("name"), "bob");
2430        sink.run_events(vec![log.into()]).await.unwrap();
2431
2432        let event = collect_n(source, 1).await.remove(0).into_log();
2433        assert_eq!(event["greeting"], "hello".into());
2434        assert_eq!(event["name"], "bob".into());
2435        assert!(event.get_timestamp().is_some());
2436        assert_eq!(
2437            event[log_schema().source_type_key().unwrap().to_string()],
2438            "splunk_hec".into()
2439        );
2440        assert!(event.metadata().splunk_hec_token().is_none());
2441    }
2442
2443    #[tokio::test]
2444    async fn json_invalid_path_event() {
2445        let (sink, source) = start(
2446            JsonSerializerConfig::default().into(),
2447            Compression::gzip_default(),
2448            None,
2449        )
2450        .await;
2451
2452        let mut log = LogEvent::default();
2453        // Test with a field that would be considered an invalid path if it were to
2454        // be treated as a path and not a simple field name.
2455        log.insert(event_path!("(greeting | thing"), "hello");
2456        sink.run_events(vec![log.into()]).await.unwrap();
2457
2458        let event = collect_n(source, 1).await.remove(0).into_log();
2459        assert_eq!(
2460            event.get(event_path!("(greeting | thing")),
2461            Some(&Value::from("hello"))
2462        );
2463    }
2464
2465    #[tokio::test]
2466    async fn line_to_message() {
2467        let (sink, source) = start(
2468            JsonSerializerConfig::default().into(),
2469            Compression::gzip_default(),
2470            None,
2471        )
2472        .await;
2473
2474        let mut event = LogEvent::default();
2475        event.insert(event_path!("line"), "hello");
2476        sink.run_events(vec![event.into()]).await.unwrap();
2477
2478        let event = collect_n(source, 1).await.remove(0);
2479        assert_eq!(
2480            event.as_log()[log_schema().message_key().unwrap().to_string()],
2481            "hello".into()
2482        );
2483        assert!(event.metadata().splunk_hec_token().is_none());
2484    }
2485
2486    #[tokio::test]
2487    async fn raw() {
2488        assert_source_compliance(&HTTP_PUSH_SOURCE_TAGS, async {
2489            let message = "raw";
2490            let (source, address, _guard) = source(None).await;
2491
2492            assert_eq!(200, post(address, "services/collector/raw", message).await);
2493
2494            let event = collect_n(source, 1).await.remove(0);
2495            assert_eq!(
2496                event.as_log()[log_schema().message_key().unwrap().to_string()],
2497                message.into()
2498            );
2499            assert_eq!(event.as_log()[&super::CHANNEL], "channel".into());
2500            assert!(event.as_log().get_timestamp().is_some());
2501            assert_eq!(
2502                event.as_log()[log_schema().source_type_key().unwrap().to_string()],
2503                "splunk_hec".into()
2504            );
2505            assert!(event.metadata().splunk_hec_token().is_none());
2506        })
2507        .await;
2508    }
2509
2510    #[tokio::test]
2511    async fn root() {
2512        assert_source_compliance(&HTTP_PUSH_SOURCE_TAGS, async {
2513            let message = r#"{ "event": { "message": "root"} }"#;
2514            let (source, address, _guard) = source(None).await;
2515
2516            assert_eq!(200, post(address, "services/collector", message).await);
2517
2518            let event = collect_n(source, 1).await.remove(0);
2519            assert_eq!(
2520                event.as_log()[log_schema().message_key().unwrap().to_string()],
2521                "root".into()
2522            );
2523            assert_eq!(event.as_log()[&super::CHANNEL], "channel".into());
2524            assert!(event.as_log().get_timestamp().is_some());
2525            assert_eq!(
2526                event.as_log()[log_schema().source_type_key().unwrap().to_string()],
2527                "splunk_hec".into()
2528            );
2529            assert!(event.metadata().splunk_hec_token().is_none());
2530        })
2531        .await;
2532    }
2533
2534    #[tokio::test]
2535    async fn channel_header() {
2536        assert_source_compliance(&HTTP_PUSH_SOURCE_TAGS, async {
2537            let message = "raw";
2538            let (source, address, _guard) = source(None).await;
2539
2540            let opts = SendWithOpts {
2541                channel: Some(Channel::Header("guid")),
2542                forwarded_for: None,
2543            };
2544
2545            assert_eq!(
2546                200,
2547                send_with(address, "services/collector/raw", message, TOKEN, &opts).await
2548            );
2549
2550            let event = collect_n(source, 1).await.remove(0);
2551            assert_eq!(event.as_log()[&super::CHANNEL], "guid".into());
2552        })
2553        .await;
2554    }
2555
2556    #[tokio::test]
2557    async fn xff_header_raw() {
2558        assert_source_compliance(&HTTP_PUSH_SOURCE_TAGS, async {
2559            let message = "raw";
2560            let (source, address, _guard) = source(None).await;
2561
2562            let opts = SendWithOpts {
2563                channel: Some(Channel::Header("guid")),
2564                forwarded_for: Some(String::from("10.0.0.1")),
2565            };
2566
2567            assert_eq!(
2568                200,
2569                send_with(address, "services/collector/raw", message, TOKEN, &opts).await
2570            );
2571
2572            let event = collect_n(source, 1).await.remove(0);
2573            assert_eq!(
2574                event.as_log()[log_schema().host_key().unwrap().to_string().as_str()],
2575                "10.0.0.1".into()
2576            );
2577        })
2578        .await;
2579    }
2580
2581    // Test helps to illustrate that a payload's `host` value should override an x-forwarded-for header
2582    #[tokio::test]
2583    async fn xff_header_event_with_host_field() {
2584        assert_source_compliance(&HTTP_PUSH_SOURCE_TAGS, async {
2585            let message = r#"{"event":"first", "host": "10.1.0.2"}"#;
2586            let (source, address, _guard) = source(None).await;
2587
2588            let opts = SendWithOpts {
2589                channel: Some(Channel::Header("guid")),
2590                forwarded_for: Some(String::from("10.0.0.1")),
2591            };
2592
2593            assert_eq!(
2594                200,
2595                send_with(address, "services/collector/event", message, TOKEN, &opts).await
2596            );
2597
2598            let event = collect_n(source, 1).await.remove(0);
2599            assert_eq!(
2600                event.as_log()[log_schema().host_key().unwrap().to_string().as_str()],
2601                "10.1.0.2".into()
2602            );
2603        })
2604        .await;
2605    }
2606
2607    // Test helps to illustrate that a payload's `host` value should override an x-forwarded-for header
2608    #[tokio::test]
2609    async fn xff_header_event_without_host_field() {
2610        assert_source_compliance(&HTTP_PUSH_SOURCE_TAGS, async {
2611            let message = r#"{"event":"first", "color": "blue"}"#;
2612            let (source, address, _guard) = source(None).await;
2613
2614            let opts = SendWithOpts {
2615                channel: Some(Channel::Header("guid")),
2616                forwarded_for: Some(String::from("10.0.0.1")),
2617            };
2618
2619            assert_eq!(
2620                200,
2621                send_with(address, "services/collector/event", message, TOKEN, &opts).await
2622            );
2623
2624            let event = collect_n(source, 1).await.remove(0);
2625            assert_eq!(
2626                event.as_log()[log_schema().host_key().unwrap().to_string().as_str()],
2627                "10.0.0.1".into()
2628            );
2629        })
2630        .await;
2631    }
2632
2633    #[tokio::test]
2634    async fn channel_query_param() {
2635        assert_source_compliance(&HTTP_PUSH_SOURCE_TAGS, async {
2636            let message = "raw";
2637            let (source, address, _guard) = source(None).await;
2638
2639            let opts = SendWithOpts {
2640                channel: Some(Channel::QueryParam("guid")),
2641                forwarded_for: None,
2642            };
2643
2644            assert_eq!(
2645                200,
2646                send_with(address, "services/collector/raw", message, TOKEN, &opts).await
2647            );
2648
2649            let event = collect_n(source, 1).await.remove(0);
2650            assert_eq!(event.as_log()[&super::CHANNEL], "guid".into());
2651        })
2652        .await;
2653    }
2654
2655    #[tokio::test]
2656    async fn no_data() {
2657        let (_source, address, _guard) = source(None).await;
2658
2659        assert_eq!(400, post(address, "services/collector/event", "").await);
2660    }
2661
2662    #[tokio::test]
2663    async fn invalid_token() {
2664        assert_source_error(&COMPONENT_ERROR_TAGS, async {
2665            let (_source, address, _guard) = source(None).await;
2666            let opts = SendWithOpts {
2667                channel: Some(Channel::Header("channel")),
2668                forwarded_for: None,
2669            };
2670
2671            assert_eq!(
2672                401,
2673                send_with(address, "services/collector/event", "", "nope", &opts).await
2674            );
2675        })
2676        .await;
2677    }
2678
2679    #[tokio::test]
2680    async fn health_ignores_token() {
2681        let (_source, address, _guard) = source(None).await;
2682
2683        let res = reqwest::Client::new()
2684            .get(format!("http://{address}/services/collector/health"))
2685            .header("Authorization", format!("Splunk {}", "invalid token"))
2686            .send()
2687            .await
2688            .unwrap();
2689
2690        assert_eq!(200, res.status().as_u16());
2691    }
2692
2693    #[tokio::test]
2694    async fn health() {
2695        let (_source, address, _guard) = source(None).await;
2696
2697        let res = reqwest::Client::new()
2698            .get(format!("http://{address}/services/collector/health"))
2699            .send()
2700            .await
2701            .unwrap();
2702
2703        assert_eq!(200, res.status().as_u16());
2704    }
2705
2706    #[tokio::test]
2707    async fn secondary_token() {
2708        assert_source_compliance(&HTTP_PUSH_SOURCE_TAGS, async {
2709            let message = r#"{"event":"first", "color": "blue"}"#;
2710            let (_source, address, _guard) =
2711                source_with(None, Some(VALID_TOKENS), None, false).await;
2712            let options = SendWithOpts {
2713                channel: None,
2714                forwarded_for: None,
2715            };
2716
2717            assert_eq!(
2718                200,
2719                send_with(
2720                    address,
2721                    "services/collector/event",
2722                    message,
2723                    VALID_TOKENS.get(1).unwrap(),
2724                    &options
2725                )
2726                .await
2727            );
2728        })
2729        .await;
2730    }
2731
2732    #[tokio::test]
2733    async fn event_service_token_passthrough_enabled() {
2734        assert_source_compliance(&HTTP_PUSH_SOURCE_TAGS, async {
2735            let message = "passthrough_token_enabled";
2736            let (source, address, _guard) = source_with(None, Some(VALID_TOKENS), None, true).await;
2737            let (sink, health) = sink(
2738                address,
2739                TextSerializerConfig::default().into(),
2740                Compression::gzip_default(),
2741            )
2742            .await;
2743            assert!(health.await.is_ok());
2744
2745            let event = channel_n(vec![message], sink, source).await.remove(0);
2746
2747            assert_eq!(
2748                event.as_log()[log_schema().message_key().unwrap().to_string()],
2749                message.into()
2750            );
2751            assert_eq!(
2752                event.metadata().splunk_hec_token().as_deref().unwrap(),
2753                TOKEN
2754            );
2755        })
2756        .await;
2757    }
2758
2759    #[tokio::test]
2760    async fn raw_service_token_passthrough_enabled() {
2761        assert_source_compliance(&HTTP_PUSH_SOURCE_TAGS, async {
2762            let message = "raw";
2763            let (source, address, _guard) = source_with(None, Some(VALID_TOKENS), None, true).await;
2764
2765            assert_eq!(200, post(address, "services/collector/raw", message).await);
2766
2767            let event = collect_n(source, 1).await.remove(0);
2768            assert_eq!(
2769                event.as_log()[log_schema().message_key().unwrap().to_string()],
2770                message.into()
2771            );
2772            assert_eq!(event.as_log()[&super::CHANNEL], "channel".into());
2773            assert!(event.as_log().get_timestamp().is_some());
2774            assert_eq!(
2775                event.as_log()[log_schema().source_type_key().unwrap().to_string()],
2776                "splunk_hec".into()
2777            );
2778            assert_eq!(
2779                event.metadata().splunk_hec_token().as_deref().unwrap(),
2780                TOKEN
2781            );
2782        })
2783        .await;
2784    }
2785
2786    #[tokio::test]
2787    async fn no_authorization() {
2788        assert_source_compliance(&HTTP_PUSH_SOURCE_TAGS, async {
2789            let message = "no_authorization";
2790            let (source, address, _guard) = source_with(None, None, None, false).await;
2791            let (sink, health) = sink(
2792                address,
2793                TextSerializerConfig::default().into(),
2794                Compression::gzip_default(),
2795            )
2796            .await;
2797            assert!(health.await.is_ok());
2798
2799            let event = channel_n(vec![message], sink, source).await.remove(0);
2800
2801            assert_eq!(
2802                event.as_log()[log_schema().message_key().unwrap().to_string()],
2803                message.into()
2804            );
2805            assert!(event.metadata().splunk_hec_token().is_none());
2806        })
2807        .await;
2808    }
2809
2810    #[tokio::test]
2811    async fn no_authorization_token_passthrough_enabled() {
2812        assert_source_compliance(&HTTP_PUSH_SOURCE_TAGS, async {
2813            let message = "no_authorization";
2814            let (source, address, _guard) = source_with(None, None, None, true).await;
2815            let (sink, health) = sink(
2816                address,
2817                TextSerializerConfig::default().into(),
2818                Compression::gzip_default(),
2819            )
2820            .await;
2821            assert!(health.await.is_ok());
2822
2823            let event = channel_n(vec![message], sink, source).await.remove(0);
2824
2825            assert_eq!(
2826                event.as_log()[log_schema().message_key().unwrap().to_string()],
2827                message.into()
2828            );
2829            assert_eq!(
2830                event.metadata().splunk_hec_token().as_deref().unwrap(),
2831                TOKEN
2832            );
2833        })
2834        .await;
2835    }
2836
2837    #[tokio::test]
2838    async fn partial() {
2839        assert_source_compliance(&HTTP_PUSH_SOURCE_TAGS, async {
2840            let message = r#"{"event":"first"}{"event":"second""#;
2841            let (source, address, _guard) = source(None).await;
2842
2843            assert_eq!(
2844                400,
2845                post(address, "services/collector/event", message).await
2846            );
2847
2848            let event = collect_n(source, 1).await.remove(0);
2849            assert_eq!(
2850                event.as_log()[log_schema().message_key().unwrap().to_string()],
2851                "first".into()
2852            );
2853            assert!(event.as_log().get_timestamp().is_some());
2854            assert_eq!(
2855                event.as_log()[log_schema().source_type_key().unwrap().to_string()],
2856                "splunk_hec".into()
2857            );
2858        })
2859        .await;
2860    }
2861
2862    #[tokio::test]
2863    async fn handles_newlines() {
2864        assert_source_compliance(&HTTP_PUSH_SOURCE_TAGS, async {
2865            let message = r#"
2866{"event":"first"}
2867        "#;
2868            let (source, address, _guard) = source(None).await;
2869
2870            assert_eq!(
2871                200,
2872                post(address, "services/collector/event", message).await
2873            );
2874
2875            let event = collect_n(source, 1).await.remove(0);
2876            assert_eq!(
2877                event.as_log()[log_schema().message_key().unwrap().to_string()],
2878                "first".into()
2879            );
2880            assert!(event.as_log().get_timestamp().is_some());
2881            assert_eq!(
2882                event.as_log()[log_schema().source_type_key().unwrap().to_string()],
2883                "splunk_hec".into()
2884            );
2885        })
2886        .await;
2887    }
2888
2889    #[tokio::test]
2890    async fn handles_spaces() {
2891        assert_source_compliance(&HTTP_PUSH_SOURCE_TAGS, async {
2892            let message = r#" {"event":"first"} "#;
2893            let (source, address, _guard) = source(None).await;
2894
2895            assert_eq!(
2896                200,
2897                post(address, "services/collector/event", message).await
2898            );
2899
2900            let event = collect_n(source, 1).await.remove(0);
2901            assert_eq!(
2902                event.as_log()[log_schema().message_key().unwrap().to_string()],
2903                "first".into()
2904            );
2905            assert!(event.as_log().get_timestamp().is_some());
2906            assert_eq!(
2907                event.as_log()[log_schema().source_type_key().unwrap().to_string()],
2908                "splunk_hec".into()
2909            );
2910        })
2911        .await;
2912    }
2913
2914    #[tokio::test]
2915    async fn handles_non_utf8() {
2916        assert_source_compliance(&HTTP_PUSH_SOURCE_TAGS, async {
2917        let message = b" {\"event\": { \"non\": \"A non UTF8 character \xE4\", \"number\": 2, \"bool\": true } } ";
2918        let (source, address, _guard) = source(None).await;
2919
2920        let b = reqwest::Client::new()
2921            .post(format!(
2922                "http://{}/{}",
2923                address, "services/collector/event"
2924            ))
2925            .header("Authorization", format!("Splunk {TOKEN}"))
2926            .body::<&[u8]>(message);
2927
2928        assert_eq!(200, b.send().await.unwrap().status().as_u16());
2929
2930        let event = collect_n(source, 1).await.remove(0);
2931        assert_eq!(event.as_log()["non"], "A non UTF8 character �".into());
2932        assert_eq!(event.as_log()["number"], 2.into());
2933        assert_eq!(event.as_log()["bool"], true.into());
2934        assert!(event.as_log().get((lookup::PathPrefix::Event, log_schema().timestamp_key().unwrap())).is_some());
2935        assert_eq!(
2936            event.as_log()[log_schema().source_type_key().unwrap().to_string()],
2937            "splunk_hec".into()
2938        );
2939    }).await;
2940    }
2941
2942    #[tokio::test]
2943    async fn default() {
2944        assert_source_compliance(&HTTP_PUSH_SOURCE_TAGS, async {
2945        let message = r#"{"event":"first","source":"main"}{"event":"second"}{"event":"third","source":"secondary"}"#;
2946        let (source, address, _guard) = source(None).await;
2947
2948        assert_eq!(
2949            200,
2950            post(address, "services/collector/event", message).await
2951        );
2952
2953        let events = collect_n(source, 3).await;
2954
2955        assert_eq!(
2956            events[0].as_log()[log_schema().message_key().unwrap().to_string()],
2957            "first".into()
2958        );
2959        assert_eq!(events[0].as_log()[&super::SOURCE], "main".into());
2960
2961        assert_eq!(
2962            events[1].as_log()[log_schema().message_key().unwrap().to_string()],
2963            "second".into()
2964        );
2965        assert_eq!(events[1].as_log()[&super::SOURCE], "main".into());
2966
2967        assert_eq!(
2968            events[2].as_log()[log_schema().message_key().unwrap().to_string()],
2969            "third".into()
2970        );
2971        assert_eq!(events[2].as_log()[&super::SOURCE], "secondary".into());
2972    }).await;
2973    }
2974
2975    #[test]
2976    fn parse_timestamps() {
2977        let cases = vec![
2978            Utc::now(),
2979            Utc.with_ymd_and_hms(1971, 11, 7, 1, 1, 1)
2980                .single()
2981                .expect("invalid timestamp"),
2982            Utc.with_ymd_and_hms(2011, 8, 5, 1, 1, 1)
2983                .single()
2984                .expect("invalid timestamp"),
2985            Utc.with_ymd_and_hms(2189, 11, 4, 2, 2, 2)
2986                .single()
2987                .expect("invalid timestamp"),
2988        ];
2989
2990        for case in cases {
2991            let sec = case.timestamp();
2992            let millis = case.timestamp_millis();
2993            let nano = case.timestamp_nanos_opt().expect("Timestamp out of range");
2994
2995            assert_eq!(parse_timestamp(sec).unwrap().timestamp(), case.timestamp());
2996            assert_eq!(
2997                parse_timestamp(millis).unwrap().timestamp_millis(),
2998                case.timestamp_millis()
2999            );
3000            assert_eq!(
3001                parse_timestamp(nano)
3002                    .unwrap()
3003                    .timestamp_nanos_opt()
3004                    .unwrap(),
3005                case.timestamp_nanos_opt().expect("Timestamp out of range")
3006            );
3007        }
3008
3009        assert!(parse_timestamp(-1).is_none());
3010    }
3011
3012    /// This test will fail once `warp` crate fixes support for
3013    /// custom connection listener, at that point this test can be
3014    /// modified to pass.
3015    /// https://github.com/vectordotdev/vector/issues/7097
3016    /// https://github.com/seanmonstar/warp/issues/830
3017    /// https://github.com/seanmonstar/warp/pull/713
3018    #[tokio::test]
3019    async fn host_test() {
3020        assert_source_compliance(&HTTP_PUSH_SOURCE_TAGS, async {
3021            let message = "for the host";
3022            let (sink, source) = start(
3023                TextSerializerConfig::default().into(),
3024                Compression::gzip_default(),
3025                None,
3026            )
3027            .await;
3028
3029            let event = channel_n(vec![message], sink, source).await.remove(0);
3030
3031            assert_eq!(
3032                event.as_log()[log_schema().message_key().unwrap().to_string()],
3033                message.into()
3034            );
3035            assert!(
3036                event
3037                    .as_log()
3038                    .get((PathPrefix::Event, log_schema().host_key().unwrap()))
3039                    .is_none()
3040            );
3041        })
3042        .await;
3043    }
3044
3045    #[derive(Deserialize)]
3046    struct HecAckEventResponse {
3047        text: String,
3048        code: u8,
3049        #[serde(rename = "ackId")]
3050        ack_id: u64,
3051    }
3052
3053    #[tokio::test]
3054    async fn ack_json_event() {
3055        let ack_config = HecAcknowledgementsConfig {
3056            enabled: Some(true),
3057            ..Default::default()
3058        };
3059        let (source, address, _guard) = source(Some(ack_config)).await;
3060        let event_message = r#"{"event":"first", "color": "blue"}{"event":"second"}"#;
3061        let opts = SendWithOpts {
3062            channel: Some(Channel::Header("guid")),
3063            forwarded_for: None,
3064        };
3065        let event_res = send_with_response(
3066            address,
3067            "services/collector/event",
3068            event_message,
3069            TOKEN,
3070            &opts,
3071        )
3072        .await
3073        .json::<HecAckEventResponse>()
3074        .await
3075        .unwrap();
3076        assert_eq!("Success", event_res.text.as_str());
3077        assert_eq!(0, event_res.code);
3078        _ = collect_n(source, 1).await;
3079
3080        let ack_message = serde_json::to_string(&HecAckStatusRequest {
3081            acks: vec![event_res.ack_id],
3082        })
3083        .unwrap();
3084        let ack_res = send_with_response(
3085            address,
3086            "services/collector/ack",
3087            ack_message.as_str(),
3088            TOKEN,
3089            &opts,
3090        )
3091        .await
3092        .json::<HecAckStatusResponse>()
3093        .await
3094        .unwrap();
3095        assert!(ack_res.acks.get(&event_res.ack_id).unwrap());
3096    }
3097
3098    #[tokio::test]
3099    async fn ack_raw_event() {
3100        let ack_config = HecAcknowledgementsConfig {
3101            enabled: Some(true),
3102            ..Default::default()
3103        };
3104        let (source, address, _guard) = source(Some(ack_config)).await;
3105        let event_message = "raw event message";
3106        let opts = SendWithOpts {
3107            channel: Some(Channel::Header("guid")),
3108            forwarded_for: None,
3109        };
3110        let event_res = send_with_response(
3111            address,
3112            "services/collector/raw",
3113            event_message,
3114            TOKEN,
3115            &opts,
3116        )
3117        .await
3118        .json::<HecAckEventResponse>()
3119        .await
3120        .unwrap();
3121        assert_eq!("Success", event_res.text.as_str());
3122        assert_eq!(0, event_res.code);
3123        _ = collect_n(source, 1).await;
3124
3125        let ack_message = serde_json::to_string(&HecAckStatusRequest {
3126            acks: vec![event_res.ack_id],
3127        })
3128        .unwrap();
3129        let ack_res = send_with_response(
3130            address,
3131            "services/collector/ack",
3132            ack_message.as_str(),
3133            TOKEN,
3134            &opts,
3135        )
3136        .await
3137        .json::<HecAckStatusResponse>()
3138        .await
3139        .unwrap();
3140        assert!(ack_res.acks.get(&event_res.ack_id).unwrap());
3141    }
3142
3143    #[tokio::test]
3144    async fn ack_repeat_ack_query() {
3145        let ack_config = HecAcknowledgementsConfig {
3146            enabled: Some(true),
3147            ..Default::default()
3148        };
3149        let (source, address, _guard) = source(Some(ack_config)).await;
3150        let event_message = "raw event message";
3151        let opts = SendWithOpts {
3152            channel: Some(Channel::Header("guid")),
3153            forwarded_for: None,
3154        };
3155        let event_res = send_with_response(
3156            address,
3157            "services/collector/raw",
3158            event_message,
3159            TOKEN,
3160            &opts,
3161        )
3162        .await
3163        .json::<HecAckEventResponse>()
3164        .await
3165        .unwrap();
3166        _ = collect_n(source, 1).await;
3167
3168        let ack_message = serde_json::to_string(&HecAckStatusRequest {
3169            acks: vec![event_res.ack_id],
3170        })
3171        .unwrap();
3172        let ack_res = send_with_response(
3173            address,
3174            "services/collector/ack",
3175            ack_message.as_str(),
3176            TOKEN,
3177            &opts,
3178        )
3179        .await
3180        .json::<HecAckStatusResponse>()
3181        .await
3182        .unwrap();
3183        assert!(ack_res.acks.get(&event_res.ack_id).unwrap());
3184
3185        let ack_res = send_with_response(
3186            address,
3187            "services/collector/ack",
3188            ack_message.as_str(),
3189            TOKEN,
3190            &opts,
3191        )
3192        .await
3193        .json::<HecAckStatusResponse>()
3194        .await
3195        .unwrap();
3196        assert!(!ack_res.acks.get(&event_res.ack_id).unwrap());
3197    }
3198
3199    #[tokio::test]
3200    async fn ack_exceed_max_number_of_ack_channels() {
3201        let ack_config = HecAcknowledgementsConfig {
3202            enabled: Some(true),
3203            max_number_of_ack_channels: NonZeroU64::new(1).unwrap(),
3204            ..Default::default()
3205        };
3206
3207        let (_source, address, _guard) = source(Some(ack_config)).await;
3208        let mut opts = SendWithOpts {
3209            channel: Some(Channel::Header("guid")),
3210            forwarded_for: None,
3211        };
3212        assert_eq!(
3213            200,
3214            send_with(address, "services/collector/raw", "message", TOKEN, &opts).await
3215        );
3216
3217        opts.channel = Some(Channel::Header("other-guid"));
3218        assert_eq!(
3219            503,
3220            send_with(address, "services/collector/raw", "message", TOKEN, &opts).await
3221        );
3222        assert_eq!(
3223            503,
3224            send_with(
3225                address,
3226                "services/collector/event",
3227                r#"{"event":"first"}"#,
3228                TOKEN,
3229                &opts
3230            )
3231            .await
3232        );
3233    }
3234
3235    #[tokio::test]
3236    async fn ack_exceed_max_pending_acks_per_channel() {
3237        let ack_config = HecAcknowledgementsConfig {
3238            enabled: Some(true),
3239            max_pending_acks_per_channel: NonZeroU64::new(1).unwrap(),
3240            ..Default::default()
3241        };
3242
3243        let (source, address, _guard) = source(Some(ack_config)).await;
3244        let opts = SendWithOpts {
3245            channel: Some(Channel::Header("guid")),
3246            forwarded_for: None,
3247        };
3248        for _ in 0..5 {
3249            send_with(
3250                address,
3251                "services/collector/event",
3252                r#"{"event":"first"}"#,
3253                TOKEN,
3254                &opts,
3255            )
3256            .await;
3257        }
3258        for _ in 0..5 {
3259            send_with(address, "services/collector/raw", "message", TOKEN, &opts).await;
3260        }
3261        let event_res = send_with_response(
3262            address,
3263            "services/collector/event",
3264            r#"{"event":"this will be acked"}"#,
3265            TOKEN,
3266            &opts,
3267        )
3268        .await
3269        .json::<HecAckEventResponse>()
3270        .await
3271        .unwrap();
3272        _ = collect_n(source, 11).await;
3273
3274        let ack_message_dropped = serde_json::to_string(&HecAckStatusRequest {
3275            acks: (0..10).collect::<Vec<u64>>(),
3276        })
3277        .unwrap();
3278        let ack_res = send_with_response(
3279            address,
3280            "services/collector/ack",
3281            ack_message_dropped.as_str(),
3282            TOKEN,
3283            &opts,
3284        )
3285        .await
3286        .json::<HecAckStatusResponse>()
3287        .await
3288        .unwrap();
3289        assert!(ack_res.acks.values().all(|ack_status| !*ack_status));
3290
3291        let ack_message_acked = serde_json::to_string(&HecAckStatusRequest {
3292            acks: vec![event_res.ack_id],
3293        })
3294        .unwrap();
3295        let ack_res = send_with_response(
3296            address,
3297            "services/collector/ack",
3298            ack_message_acked.as_str(),
3299            TOKEN,
3300            &opts,
3301        )
3302        .await
3303        .json::<HecAckStatusResponse>()
3304        .await
3305        .unwrap();
3306        assert!(ack_res.acks.get(&event_res.ack_id).unwrap());
3307    }
3308
3309    #[tokio::test]
3310    async fn ack_service_accepts_parameterized_content_type() {
3311        let ack_config = HecAcknowledgementsConfig {
3312            enabled: Some(true),
3313            ..Default::default()
3314        };
3315        let (source, address, _guard) = source(Some(ack_config)).await;
3316        let opts = SendWithOpts {
3317            channel: Some(Channel::Header("guid")),
3318            forwarded_for: None,
3319        };
3320
3321        let event_res = send_with_response(
3322            address,
3323            "services/collector/event",
3324            r#"{"event":"param-test"}"#,
3325            TOKEN,
3326            &opts,
3327        )
3328        .await
3329        .json::<HecAckEventResponse>()
3330        .await
3331        .unwrap();
3332        let _ = collect_n(source, 1).await;
3333
3334        let body = serde_json::to_string(&HecAckStatusRequest {
3335            acks: vec![event_res.ack_id],
3336        })
3337        .unwrap();
3338
3339        let res = reqwest::Client::new()
3340            .post(format!("http://{address}/services/collector/ack"))
3341            .header("Authorization", format!("Splunk {TOKEN}"))
3342            .header("x-splunk-request-channel", "guid")
3343            .header("Content-Type", "application/json; some-random-text; hello")
3344            .body(body)
3345            .send()
3346            .await
3347            .unwrap();
3348
3349        assert_eq!(200, res.status().as_u16());
3350
3351        let _parsed: HecAckStatusResponse = res.json().await.unwrap();
3352    }
3353
3354    #[tokio::test]
3355    async fn event_service_acknowledgements_enabled_channel_required() {
3356        let message = r#"{"event":"first", "color": "blue"}"#;
3357        let ack_config = HecAcknowledgementsConfig {
3358            enabled: Some(true),
3359            ..Default::default()
3360        };
3361        let (_, address, _guard) = source(Some(ack_config)).await;
3362
3363        let opts = SendWithOpts {
3364            channel: None,
3365            forwarded_for: None,
3366        };
3367
3368        assert_eq!(
3369            400,
3370            send_with(address, "services/collector/event", message, TOKEN, &opts).await
3371        );
3372    }
3373
3374    #[tokio::test]
3375    async fn ack_service_acknowledgements_disabled() {
3376        let message = r#" {"acks":[0]} "#;
3377        let (_, address, _guard) = source(None).await;
3378
3379        let opts = SendWithOpts {
3380            channel: Some(Channel::Header("guid")),
3381            forwarded_for: None,
3382        };
3383
3384        assert_eq!(
3385            400,
3386            send_with(address, "services/collector/ack", message, TOKEN, &opts).await
3387        );
3388    }
3389
3390    async fn source_with_codec(
3391        event: CodecConfig,
3392        raw: CodecConfig,
3393    ) -> (
3394        impl Stream<Item = Event> + Unpin + use<>,
3395        SocketAddr,
3396        PortGuard,
3397    ) {
3398        let (sender, recv) = SourceSender::new_test_finalize(EventStatus::Delivered);
3399        let (_guard, address) = next_addr();
3400        let cx = SourceContext::new_test(sender, None);
3401        tokio::spawn(async move {
3402            SplunkConfig {
3403                address,
3404                token: Some(TOKEN.to_owned().into()),
3405                valid_tokens: None,
3406                tls: None,
3407                acknowledgements: Default::default(),
3408                store_hec_token: false,
3409                log_namespace: None,
3410                keepalive: Default::default(),
3411                event,
3412                raw,
3413            }
3414            .build(cx)
3415            .await
3416            .unwrap()
3417            .await
3418            .unwrap()
3419        });
3420        wait_for_tcp(address).await;
3421        (recv, address, _guard)
3422    }
3423
3424    /// Codec config that just sets `decoding` (default framing).
3425    fn codec_decoding(decoding: DeserializerConfig) -> CodecConfig {
3426        CodecConfig {
3427            framing: None,
3428            decoding: Some(decoding),
3429        }
3430    }
3431
3432    /// Codec config that sets both `framing` and `decoding`.
3433    fn codec_full(
3434        framing: Option<FramingConfig>,
3435        decoding: Option<DeserializerConfig>,
3436    ) -> CodecConfig {
3437        CodecConfig { framing, decoding }
3438    }
3439
3440    #[tokio::test]
3441    async fn decoder_event_endpoint_json_string() {
3442        assert_source_compliance(&HTTP_PUSH_SOURCE_TAGS, async {
3443            let (source, address, _guard) = source_with_codec(
3444                codec_decoding(vector_lib::codecs::JsonDeserializerConfig::default().into()),
3445                CodecConfig::default(),
3446            )
3447            .await;
3448            let envelope =
3449                r#"{"event":"{\"foo\":\"bar\",\"n\":42}","host":"client-host","sourcetype":"my-app"}"#;
3450            assert_eq!(
3451                200,
3452                post(address, "services/collector/event", envelope).await
3453            );
3454
3455            let event = collect_n(source, 1).await.remove(0);
3456            let log = event.as_log();
3457            assert_eq!(log["foo"], "bar".into());
3458            assert_eq!(log["n"], 42.into());
3459            assert_eq!(
3460                log[log_schema().host_key().unwrap().to_string().as_str()],
3461                "client-host".into()
3462            );
3463            assert_eq!(log[&super::SOURCETYPE], "my-app".into());
3464        })
3465        .await;
3466    }
3467
3468    #[tokio::test]
3469    async fn decoder_event_endpoint_json_object_round_trip() {
3470        assert_source_compliance(&HTTP_PUSH_SOURCE_TAGS, async {
3471            let (source, address, _guard) = source_with_codec(
3472                codec_decoding(vector_lib::codecs::JsonDeserializerConfig::default().into()),
3473                CodecConfig::default(),
3474            )
3475            .await;
3476            let envelope = r#"{"event":{"foo":"bar","nested":{"k":1}},"host":"h"}"#;
3477            assert_eq!(
3478                200,
3479                post(address, "services/collector/event", envelope).await
3480            );
3481
3482            let event = collect_n(source, 1).await.remove(0);
3483            let log = event.as_log();
3484            assert_eq!(log["foo"], "bar".into());
3485            assert_eq!(*log.get(event_path!("nested", "k")).unwrap(), 1.into());
3486            assert_eq!(
3487                log[log_schema().host_key().unwrap().to_string().as_str()],
3488                "h".into()
3489            );
3490        })
3491        .await;
3492    }
3493
3494    #[tokio::test]
3495    async fn decoder_event_endpoint_all_envelope_fields_yield_to_decoder() {
3496        // The decoded path must defer to the codec for `splunk_channel`,
3497        // `splunk_index`, `splunk_source`, and `splunk_sourcetype` in legacy ns -
3498        // not just `host`. Otherwise the changelog's "decoder wins on conflict"
3499        // promise is broken for HEC envelope metadata.
3500        assert_source_compliance(&HTTP_PUSH_SOURCE_TAGS, async {
3501            let (source, address, _guard) = source_with_codec(
3502                codec_decoding(vector_lib::codecs::JsonDeserializerConfig::default().into()),
3503                CodecConfig::default(),
3504            )
3505            .await;
3506            // The string `event` decodes to a JSON object that pre-populates each
3507            // legacy splunk_* field. The envelope sets conflicting values for the
3508            // same fields and must lose.
3509            let envelope = r#"{
3510                "event":"{\"splunk_channel\":\"decoder-channel\",\"splunk_index\":\"decoder-index\",\"splunk_source\":\"decoder-source\",\"splunk_sourcetype\":\"decoder-sourcetype\"}",
3511                "index":"envelope-index",
3512                "source":"envelope-source",
3513                "sourcetype":"envelope-sourcetype"
3514            }"#;
3515            assert_eq!(
3516                200,
3517                post(address, "services/collector/event", envelope).await
3518            );
3519
3520            let event = collect_n(source, 1).await.remove(0);
3521            let log = event.as_log();
3522            assert_eq!(log[&super::CHANNEL], "decoder-channel".into());
3523            assert_eq!(log[&super::INDEX], "decoder-index".into());
3524            assert_eq!(log[&super::SOURCE], "decoder-source".into());
3525            assert_eq!(log[&super::SOURCETYPE], "decoder-sourcetype".into());
3526        })
3527        .await;
3528    }
3529
3530    #[tokio::test]
3531    async fn decoder_event_endpoint_decoder_field_wins_over_envelope() {
3532        assert_source_compliance(&HTTP_PUSH_SOURCE_TAGS, async {
3533            let (source, address, _guard) = source_with_codec(
3534                codec_decoding(vector_lib::codecs::JsonDeserializerConfig::default().into()),
3535                CodecConfig::default(),
3536            )
3537            .await;
3538            // The string `event` decodes to {host: "decoder-host"}; the envelope sets
3539            // host: "envelope-host". The decoder's value must win.
3540            let envelope = r#"{"event":"{\"host\":\"decoder-host\"}","host":"envelope-host"}"#;
3541            assert_eq!(
3542                200,
3543                post(address, "services/collector/event", envelope).await
3544            );
3545
3546            let event = collect_n(source, 1).await.remove(0);
3547            let log = event.as_log();
3548            assert_eq!(
3549                log[log_schema().host_key().unwrap().to_string().as_str()],
3550                "decoder-host".into()
3551            );
3552        })
3553        .await;
3554    }
3555
3556    #[tokio::test]
3557    async fn decoder_event_endpoint_decode_failure_returns_200() {
3558        // A malformed inner JSON must not surface as an HTTP error to the Splunk
3559        // client - decode failures are swallowed by the codec like other Vector
3560        // sources do.
3561        let (_source, address, _guard) = source_with_codec(
3562            codec_decoding(vector_lib::codecs::JsonDeserializerConfig::default().into()),
3563            CodecConfig::default(),
3564        )
3565        .await;
3566        let envelope = r#"{"event":"not valid json {","host":"h"}"#;
3567        assert_eq!(
3568            200,
3569            post(address, "services/collector/event", envelope).await
3570        );
3571    }
3572
3573    #[tokio::test]
3574    async fn decoder_raw_endpoint_newline_delimited() {
3575        assert_source_compliance(&HTTP_PUSH_SOURCE_TAGS, async {
3576            let (source, address, _guard) = source_with_codec(
3577                CodecConfig::default(),
3578                codec_full(
3579                    Some(FramingConfig::NewlineDelimited(Default::default())),
3580                    Some(DeserializerConfig::Bytes),
3581                ),
3582            )
3583            .await;
3584            let body = "line1\nline2\nline3";
3585            assert_eq!(200, post(address, "services/collector/raw", body).await);
3586
3587            let events = collect_n(source, 3).await;
3588            assert_eq!(events.len(), 3);
3589            let messages: Vec<String> = events
3590                .iter()
3591                .map(|e| {
3592                    e.as_log()[log_schema().message_key().unwrap().to_string()]
3593                        .to_string_lossy()
3594                        .into_owned()
3595                })
3596                .collect();
3597            assert!(messages.contains(&"line1".to_string()));
3598            assert!(messages.contains(&"line2".to_string()));
3599            assert!(messages.contains(&"line3".to_string()));
3600
3601            // All events share the channel from the request header.
3602            for event in &events {
3603                assert_eq!(event.as_log()[&super::CHANNEL], "channel".into());
3604            }
3605        })
3606        .await;
3607    }
3608
3609    #[tokio::test]
3610    async fn decoder_event_endpoint_envelope_without_time_has_fallback_timestamp() {
3611        // Regression: with a decoder set, an envelope that omits `time` must still
3612        // produce events with a timestamp (the legacy /event path always wrote one).
3613        assert_source_compliance(&HTTP_PUSH_SOURCE_TAGS, async {
3614            let (source, address, _guard) = source_with_codec(
3615                codec_decoding(vector_lib::codecs::JsonDeserializerConfig::default().into()),
3616                CodecConfig::default(),
3617            )
3618            .await;
3619            let envelope = r#"{"event":"{\"foo\":\"bar\"}"}"#;
3620            assert_eq!(
3621                200,
3622                post(address, "services/collector/event", envelope).await
3623            );
3624
3625            let event = collect_n(source, 1).await.remove(0);
3626            assert!(
3627                event.as_log().get_timestamp().is_some(),
3628                "decoded event from envelope without `time` field is missing a timestamp"
3629            );
3630        })
3631        .await;
3632    }
3633
3634    #[tokio::test]
3635    async fn decoder_independent_per_endpoint_codecs() {
3636        // /event and /raw can be configured with completely different codecs and
3637        // each endpoint applies only its own. Here /event uses JSON decoding (so a
3638        // string `event` field decodes to fields) and /raw uses newline framing
3639        // with a bytes decoder (so a multi-line body fans out to N events).
3640        assert_source_compliance(&HTTP_PUSH_SOURCE_TAGS, async {
3641            let (source, address, _guard) = source_with_codec(
3642                codec_decoding(vector_lib::codecs::JsonDeserializerConfig::default().into()),
3643                codec_full(
3644                    Some(FramingConfig::NewlineDelimited(Default::default())),
3645                    Some(DeserializerConfig::Bytes),
3646                ),
3647            )
3648            .await;
3649
3650            // /event: JSON decoder turns the inner string into structured fields.
3651            assert_eq!(
3652                200,
3653                post(
3654                    address,
3655                    "services/collector/event",
3656                    r#"{"event":"{\"foo\":\"bar\"}"}"#
3657                )
3658                .await
3659            );
3660            // /raw: newline framing splits the body into three events.
3661            assert_eq!(
3662                200,
3663                post(address, "services/collector/raw", "a\nb\nc").await
3664            );
3665
3666            let events = collect_n(source, 4).await;
3667            assert_eq!(events.len(), 4);
3668
3669            // The /event request produces one log with `foo=bar`.
3670            let event_log = events
3671                .iter()
3672                .find(|e| e.as_log().contains(event_path!("foo")))
3673                .expect("expected /event request to produce a log with `foo` set");
3674            assert_eq!(event_log.as_log()["foo"], "bar".into());
3675
3676            // The /raw request produces three logs whose messages are the lines.
3677            let raw_messages: Vec<String> = events
3678                .iter()
3679                .filter(|e| !e.as_log().contains(event_path!("foo")))
3680                .map(|e| {
3681                    e.as_log()[log_schema().message_key().unwrap().to_string()]
3682                        .to_string_lossy()
3683                        .into_owned()
3684                })
3685                .collect();
3686            assert_eq!(raw_messages.len(), 3);
3687            assert!(raw_messages.contains(&"a".to_string()));
3688            assert!(raw_messages.contains(&"b".to_string()));
3689            assert!(raw_messages.contains(&"c".to_string()));
3690        })
3691        .await;
3692    }
3693
3694    /// End-to-end test for the second-stage VRL decoder on `/services/collector/event`.
3695    ///
3696    /// Validates the core use case from PR #25312: a VRL program decodes the
3697    /// inner `event` payload *and* reads HEC envelope metadata injected before
3698    /// decoding via `%splunk_hec.*` paths.
3699    #[tokio::test]
3700    async fn decoder_vrl_reads_envelope_metadata() {
3701        assert_source_compliance(&HTTP_PUSH_SOURCE_TAGS, async {
3702            let vrl_source = r#"
3703                # Read envelope metadata injected before this VRL program runs.
3704                .envelope_host = string!(%splunk_hec.host)
3705                .envelope_sourcetype = string!(%splunk_hec.sourcetype)
3706
3707                # Decode the inner JSON payload (the bytes of the `event` string).
3708                . = merge!(parse_json!(string!(.message)), .)
3709            "#;
3710
3711            let event_codec = codec_decoding(
3712                DeserializerConfig::Vrl(VrlDeserializerConfig {
3713                    vrl: VrlDeserializerOptions {
3714                        source: vrl_source.into(),
3715                        timezone: None,
3716                    },
3717                }),
3718            );
3719
3720            let (source, address, _guard) =
3721                source_with_codec(event_codec, CodecConfig::default()).await;
3722
3723            // Send a HEC event whose `event` field is a JSON-encoded string.
3724            // The VRL decoder should parse it and also read the envelope host/sourcetype.
3725            let payload = r#"{"event":"{\"level\":\"info\",\"msg\":\"hello\"}","host":"splunk-host","sourcetype":"my-app"}"#;
3726            assert_eq!(
3727                200,
3728                post(address, "services/collector/event", payload).await
3729            );
3730
3731            let event = collect_n(source, 1).await.remove(0);
3732            let log = event.as_log();
3733
3734            // Inner JSON decoded correctly.
3735            assert_eq!(log["level"], "info".into());
3736            assert_eq!(log["msg"], "hello".into());
3737
3738            // VRL read envelope metadata via %splunk_hec.* and wrote it to event fields.
3739            assert_eq!(log["envelope_host"], "splunk-host".into());
3740            assert_eq!(log["envelope_sourcetype"], "my-app".into());
3741
3742            // Post-decode splunk_hec metadata still applied (host, sourcetype).
3743            assert_eq!(
3744                log[log_schema().host_key().unwrap().to_string().as_str()],
3745                "splunk-host".into()
3746            );
3747            assert_eq!(log[&super::SOURCETYPE], "my-app".into());
3748        })
3749        .await;
3750    }
3751
3752    #[tokio::test]
3753    async fn decoder_raw_endpoint_event_has_fallback_timestamp() {
3754        // Regression: decoded /raw events must carry an ingest timestamp like the
3755        // legacy raw_event path did via `insert_standard_vector_source_metadata`.
3756        assert_source_compliance(&HTTP_PUSH_SOURCE_TAGS, async {
3757            let (source, address, _guard) = source_with_codec(
3758                CodecConfig::default(),
3759                codec_full(None, Some(DeserializerConfig::Bytes)),
3760            )
3761            .await;
3762            let body = "hello";
3763            assert_eq!(200, post(address, "services/collector/raw", body).await);
3764
3765            let event = collect_n(source, 1).await.remove(0);
3766            assert!(
3767                event.as_log().get_timestamp().is_some(),
3768                "decoded /raw event is missing a timestamp"
3769            );
3770        })
3771        .await;
3772    }
3773
3774    #[tokio::test]
3775    async fn decoder_raw_endpoint_empty_decode_does_not_ack() {
3776        // Regression: when the decoder produces zero events from a raw payload and
3777        // acknowledgements are enabled, the response must not include an `ackId`
3778        // because /services/collector/ack would otherwise report success for data
3779        // Vector silently dropped.
3780        let ack_config = HecAcknowledgementsConfig {
3781            enabled: Some(true),
3782            ..Default::default()
3783        };
3784        let (sender, _recv) = SourceSender::new_test_finalize(EventStatus::Delivered);
3785        let (_guard, address) = next_addr();
3786        let cx = SourceContext::new_test(sender, None);
3787        tokio::spawn(async move {
3788            SplunkConfig {
3789                address,
3790                token: Some(TOKEN.to_owned().into()),
3791                valid_tokens: None,
3792                tls: None,
3793                acknowledgements: ack_config,
3794                store_hec_token: false,
3795                log_namespace: None,
3796                keepalive: Default::default(),
3797                event: CodecConfig::default(),
3798                raw: codec_decoding(vector_lib::codecs::JsonDeserializerConfig::default().into()),
3799            }
3800            .build(cx)
3801            .await
3802            .unwrap()
3803            .await
3804            .unwrap()
3805        });
3806        wait_for_tcp(address).await;
3807
3808        let opts = SendWithOpts {
3809            channel: Some(Channel::Header("guid")),
3810            forwarded_for: None,
3811        };
3812        // A body the JSON decoder cannot parse - codec drops it, no events emitted.
3813        let body = "not json {";
3814        let response = send_with_response(address, "services/collector/raw", body, TOKEN, &opts)
3815            .await
3816            .json::<serde_json::Value>()
3817            .await
3818            .unwrap();
3819
3820        assert_eq!(response["code"].as_u64(), Some(0), "response: {response:?}");
3821        assert!(
3822            response.get("ackId").is_none(),
3823            "expected no ackId in response when decoder produced zero events, got: {response:?}"
3824        );
3825    }
3826
3827    #[tokio::test]
3828    async fn decoder_raw_endpoint_partial_decode_does_not_ack() {
3829        // Regression: a request whose body decodes into some valid frames AND some
3830        // dropped frames (e.g., `valid \n invalid \n valid` under newline framing
3831        // with a JSON decoder) must not return an `ackId`. Otherwise
3832        // /services/collector/ack reports success for data Vector silently dropped.
3833        let ack_config = HecAcknowledgementsConfig {
3834            enabled: Some(true),
3835            ..Default::default()
3836        };
3837        let (sender, _recv) = SourceSender::new_test_finalize(EventStatus::Delivered);
3838        let (_guard, address) = next_addr();
3839        let cx = SourceContext::new_test(sender, None);
3840        tokio::spawn(async move {
3841            SplunkConfig {
3842                address,
3843                token: Some(TOKEN.to_owned().into()),
3844                valid_tokens: None,
3845                tls: None,
3846                acknowledgements: ack_config,
3847                store_hec_token: false,
3848                log_namespace: None,
3849                keepalive: Default::default(),
3850                event: CodecConfig::default(),
3851                raw: codec_full(
3852                    Some(FramingConfig::NewlineDelimited(Default::default())),
3853                    Some(vector_lib::codecs::JsonDeserializerConfig::default().into()),
3854                ),
3855            }
3856            .build(cx)
3857            .await
3858            .unwrap()
3859            .await
3860            .unwrap()
3861        });
3862        wait_for_tcp(address).await;
3863
3864        let opts = SendWithOpts {
3865            channel: Some(Channel::Header("guid")),
3866            forwarded_for: None,
3867        };
3868        // Two valid JSON frames bracketing one invalid frame.
3869        let body = "{\"valid\":1}\nnot json\n{\"valid\":2}";
3870        let response = send_with_response(address, "services/collector/raw", body, TOKEN, &opts)
3871            .await
3872            .json::<serde_json::Value>()
3873            .await
3874            .unwrap();
3875
3876        assert_eq!(response["code"].as_u64(), Some(0), "response: {response:?}");
3877        assert!(
3878            response.get("ackId").is_none(),
3879            "expected no ackId when the decoder dropped a frame mid-request, got: {response:?}"
3880        );
3881    }
3882
3883    #[tokio::test]
3884    async fn decoder_event_endpoint_error_index_matches_envelope_not_fanout() {
3885        // Regression: with the decoder fanning out one envelope into many events,
3886        // `InvalidEventNumber` in error responses must still report the failing
3887        // envelope's zero-indexed position, not the cumulative event count.
3888        let (source, address, _guard) = source_with_codec(
3889            codec_full(
3890                Some(FramingConfig::NewlineDelimited(Default::default())),
3891                Some(DeserializerConfig::Bytes),
3892            ),
3893            CodecConfig::default(),
3894        )
3895        .await;
3896        // Envelope 0 has an `event` string with three lines: with newline framing
3897        // and a bytes decoder, that fans out to three events. Envelope 1 omits
3898        // `event`, so the decoded path returns `MissingEventField { event: 1 }`.
3899        let body = "{\"event\":\"a\\nb\\nc\"}{\"foo\":\"bar\"}";
3900
3901        let opts = SendWithOpts {
3902            channel: Some(Channel::Header("guid")),
3903            forwarded_for: None,
3904        };
3905        let response =
3906            send_with_response(address, "services/collector/event", body, TOKEN, &opts).await;
3907        let status = response.status();
3908        let body: serde_json::Value = response.json().await.unwrap();
3909
3910        assert_eq!(status.as_u16(), 400, "body: {body:?}");
3911        assert_eq!(
3912            body["invalid-event-number"].as_u64(),
3913            Some(1),
3914            "expected envelope index 1 (the failing envelope), not a fan-out event index. body: {body:?}"
3915        );
3916        // Drain the partially-emitted events so the source task doesn't block.
3917        let _ = collect_n(source, 3).await;
3918    }
3919
3920    #[test]
3921    fn output_schema_definition_with_decoder_vector_namespace() {
3922        let config = SplunkConfig {
3923            log_namespace: Some(true),
3924            event: codec_decoding(vector_lib::codecs::JsonDeserializerConfig::default().into()),
3925            ..Default::default()
3926        };
3927        let definition = config
3928            .outputs(LogNamespace::Vector)
3929            .remove(0)
3930            .schema_definition(true);
3931
3932        // The decoder's schema produces `Kind::json()` at the root, the source
3933        // layers its envelope metadata fields on top, and the legacy log shape is
3934        // unioned in (since /raw has no decoder and still emits legacy events) -
3935        // contributing the `message` meaning at root.
3936        let expected_definition =
3937            Definition::new_with_default_metadata(Kind::json(), [LogNamespace::Vector])
3938                .with_meaning(OwnedTargetPath::event_root(), meaning::MESSAGE)
3939                .with_metadata_field(
3940                    &owned_value_path!("vector", "source_type"),
3941                    Kind::bytes(),
3942                    None,
3943                )
3944                .with_metadata_field(
3945                    &owned_value_path!("vector", "ingest_timestamp"),
3946                    Kind::timestamp(),
3947                    None,
3948                )
3949                .with_metadata_field(
3950                    &owned_value_path!("splunk_hec", "host"),
3951                    Kind::bytes(),
3952                    Some("host"),
3953                )
3954                .with_metadata_field(
3955                    &owned_value_path!("splunk_hec", "index"),
3956                    Kind::bytes(),
3957                    None,
3958                )
3959                .with_metadata_field(
3960                    &owned_value_path!("splunk_hec", "source"),
3961                    Kind::bytes(),
3962                    Some("service"),
3963                )
3964                .with_metadata_field(
3965                    &owned_value_path!("splunk_hec", "channel"),
3966                    Kind::bytes(),
3967                    None,
3968                )
3969                .with_metadata_field(
3970                    &owned_value_path!("splunk_hec", "sourcetype"),
3971                    Kind::bytes(),
3972                    None,
3973                );
3974
3975        assert_eq!(definition, Some(expected_definition));
3976    }
3977
3978    #[test]
3979    fn output_schema_definition_vector_namespace() {
3980        let config = SplunkConfig {
3981            log_namespace: Some(true),
3982            ..Default::default()
3983        };
3984
3985        let definition = config
3986            .outputs(LogNamespace::Vector)
3987            .remove(0)
3988            .schema_definition(true);
3989
3990        let expected_definition = Definition::new_with_default_metadata(
3991            Kind::object(Collection::empty()).or_bytes(),
3992            [LogNamespace::Vector],
3993        )
3994        .with_meaning(OwnedTargetPath::event_root(), meaning::MESSAGE)
3995        .with_metadata_field(
3996            &owned_value_path!("vector", "source_type"),
3997            Kind::bytes(),
3998            None,
3999        )
4000        .with_metadata_field(
4001            &owned_value_path!("vector", "ingest_timestamp"),
4002            Kind::timestamp(),
4003            None,
4004        )
4005        .with_metadata_field(
4006            &owned_value_path!("splunk_hec", "host"),
4007            Kind::bytes(),
4008            Some("host"),
4009        )
4010        .with_metadata_field(
4011            &owned_value_path!("splunk_hec", "index"),
4012            Kind::bytes(),
4013            None,
4014        )
4015        .with_metadata_field(
4016            &owned_value_path!("splunk_hec", "source"),
4017            Kind::bytes(),
4018            Some("service"),
4019        )
4020        .with_metadata_field(
4021            &owned_value_path!("splunk_hec", "channel"),
4022            Kind::bytes(),
4023            None,
4024        )
4025        .with_metadata_field(
4026            &owned_value_path!("splunk_hec", "sourcetype"),
4027            Kind::bytes(),
4028            None,
4029        );
4030
4031        assert_eq!(definition, Some(expected_definition));
4032    }
4033
4034    #[test]
4035    fn output_schema_definition_legacy_namespace() {
4036        let config = SplunkConfig::default();
4037        let definitions = config
4038            .outputs(LogNamespace::Legacy)
4039            .remove(0)
4040            .schema_definition(true);
4041
4042        let expected_definition = Definition::new_with_default_metadata(
4043            Kind::object(Collection::empty()),
4044            [LogNamespace::Legacy],
4045        )
4046        .with_event_field(&owned_value_path!("host"), Kind::bytes(), Some("host"))
4047        .with_event_field(
4048            &owned_value_path!("message"),
4049            Kind::bytes().or_undefined(),
4050            Some("message"),
4051        )
4052        .with_event_field(
4053            &owned_value_path!("line"),
4054            Kind::array(Collection::empty())
4055                .or_object(Collection::empty())
4056                .or_undefined(),
4057            None,
4058        )
4059        .with_event_field(&owned_value_path!("source_type"), Kind::bytes(), None)
4060        .with_event_field(&owned_value_path!("splunk_channel"), Kind::bytes(), None)
4061        .with_event_field(&owned_value_path!("splunk_index"), Kind::bytes(), None)
4062        .with_event_field(
4063            &owned_value_path!("splunk_source"),
4064            Kind::bytes(),
4065            Some("service"),
4066        )
4067        .with_event_field(&owned_value_path!("splunk_sourcetype"), Kind::bytes(), None)
4068        .with_event_field(&owned_value_path!("timestamp"), Kind::timestamp(), None);
4069
4070        assert_eq!(definitions, Some(expected_definition));
4071    }
4072
4073    impl ValidatableComponent for SplunkConfig {
4074        fn validation_configuration() -> ValidationConfiguration {
4075            let config = Self {
4076                address: default_socket_address(),
4077                ..Default::default()
4078            };
4079
4080            let listen_addr_http = format!("http://{}/services/collector/event", config.address);
4081            let uri = Uri::try_from(&listen_addr_http).expect("should not fail to parse URI");
4082
4083            let log_namespace: LogNamespace = config.log_namespace.unwrap_or_default().into();
4084            let framing = BytesDecoderConfig::new().into();
4085            let decoding = DeserializerConfig::Json(Default::default());
4086
4087            let external_resource = ExternalResource::new(
4088                ResourceDirection::Push,
4089                HttpResourceConfig::from_parts(uri, None).with_headers(HashMap::from([(
4090                    X_SPLUNK_REQUEST_CHANNEL.to_string(),
4091                    "channel".to_string(),
4092                )])),
4093                DecodingConfig::new(framing, decoding, false.into()),
4094            );
4095
4096            ValidationConfiguration::from_source(
4097                Self::NAME,
4098                log_namespace,
4099                vec![ComponentTestCaseConfig::from_source(
4100                    config,
4101                    None,
4102                    Some(external_resource),
4103                )],
4104            )
4105        }
4106    }
4107
4108    register_validatable_component!(SplunkConfig);
4109}