Skip to content
Merged
Show file tree
Hide file tree
Changes from 25 commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
805e0d4
feat(splunk_hec source): support second-stage framing and decoding
thomasqueirozb Apr 27, 2026
4034cd0
preserve original send_event/ack ordering when no decoder is set
thomasqueirozb Apr 27, 2026
ad824b1
Add authors to changelog
thomasqueirozb Apr 28, 2026
530acc0
Format
thomasqueirozb Apr 28, 2026
c6e4974
refactor(splunk_hec source): split codec config into per-endpoint eve…
thomasqueirozb Apr 28, 2026
e86b093
fix(splunk_hec source): include legacy log shape in schema when only …
thomasqueirozb Apr 28, 2026
5454d2d
address review: schema InsertIfEmpty parity + apply splunk_hec_token …
thomasqueirozb Apr 28, 2026
ff17286
feat(codecs): add Deserializer::parse_with_secrets and Decoder::with_…
thomasqueirozb Apr 28, 2026
a12389d
refactor: move metadata_template to VrlDeserializer only; no Decoder/…
thomasqueirozb Apr 29, 2026
ac4873b
feat(codecs): add inject_metadata to VRL decoder; wire up in exec source
thomasqueirozb Apr 29, 2026
aecddcf
fix(exec source): preserve VRL-written metadata; add DeserializerConf…
thomasqueirozb Apr 29, 2026
3f04870
fmt
thomasqueirozb Apr 29, 2026
bba7ae5
Add authors line to changelog
thomasqueirozb Apr 29, 2026
bdc4914
merge: bring in VRL decoder inject_metadata mechanism from decoder-me…
thomasqueirozb Apr 29, 2026
4777a95
refactor(codecs): remove inject_metadata flag; with_metadata_template…
thomasqueirozb Apr 29, 2026
08bb10b
feat(splunk_hec source): inject envelope metadata into VRL decoder be…
thomasqueirozb Apr 29, 2026
55dda8d
fmt
thomasqueirozb Apr 29, 2026
693bf61
fix(splunk_hec source): use InsertIfEmpty semantics for Vector-namesp…
thomasqueirozb Apr 29, 2026
113fc11
refactor: replace get-then-insert with log.try_insert for metadata In…
thomasqueirozb Apr 29, 2026
fa2199a
fix(splunk_hec source): reject empty event field in decoder path; ski…
thomasqueirozb Apr 29, 2026
54c512e
fix(splunk_hec source): only write splunk_hec.timestamp for /event de…
thomasqueirozb Apr 29, 2026
c993ed8
test(splunk_hec source): add VRL decoder end-to-end test covering env…
thomasqueirozb Apr 29, 2026
253b958
fix fmt + clippy
thomasqueirozb Apr 29, 2026
8094a7e
fmt
thomasqueirozb Apr 29, 2026
8a34522
fix(splunk_hec source): per-endpoint schema strategy without metadata…
thomasqueirozb Apr 29, 2026
e1f25c9
fix(splunk_hec source): apply extractors before fields in decoder pat…
thomasqueirozb May 1, 2026
095a52b
Avoid roundtrip json parsing during decoding
thomasqueirozb May 1, 2026
ea6e391
Fix stale documentation
thomasqueirozb May 1, 2026
657a791
Fix clippy by building DecodePayloadContext
thomasqueirozb May 4, 2026
17bfef1
refactor(splunk_hec source): extract validate_event_field and use raw…
thomasqueirozb May 4, 2026
5987d6f
Update documentation
thomasqueirozb May 4, 2026
27d87ea
Address docs review
thomasqueirozb May 4, 2026
7bd36cb
fix(splunk_hec source): pass string events as raw bytes to decoder
thomasqueirozb May 5, 2026
50460ce
refactor(splunk_hec source): extract register_ack helper to reduce in…
thomasqueirozb May 5, 2026
9c4e21f
EndpointCodecConfig -> CodecConfig
thomasqueirozb May 5, 2026
8cd9c59
simplify docs
thomasqueirozb May 5, 2026
0bcb9f1
Remove VrlDeserializer doc comment from `with_metadata_template`
thomasqueirozb May 6, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions changelog.d/splunk_hec_source_codec.enhancement.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
The `splunk_hec` source now accepts optional per-endpoint codec configuration via `event: { framing, decoding }` and `raw: { framing, decoding }`. When `decoding` is set on an endpoint, Vector applies a second decoding pass after the HEC envelope is parsed: on `/services/collector/event` the envelope's `event` field is fed through the codec (string contents are passed as raw bytes; objects, arrays, and other JSON values are JSON-serialized first to preserve shape), and on `/services/collector/raw` the request body is fed through the codec directly. A single payload can fan out to multiple events. Decoder-produced fields win on conflict with HEC envelope metadata, and decode failures do not surface to the Splunk client. Each endpoint is configured independently; when neither endpoint is configured the source preserves its existing behavior unchanged.
Comment thread
thomasqueirozb marked this conversation as resolved.
Outdated

authors: thomasqueirozb
16 changes: 15 additions & 1 deletion lib/codecs/src/decoding/decoder.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
use bytes::{Bytes, BytesMut};
use smallvec::SmallVec;
use vector_common::internal_event::emit;
use vector_core::{config::LogNamespace, event::Event};
use vector_core::{
config::LogNamespace,
event::{Event, EventMetadata},
};

use crate::{
decoding::format::Deserializer as _,
Expand Down Expand Up @@ -53,6 +56,17 @@ impl Decoder {
self
}

/// Attaches a per-decode-call metadata template to the inner deserializer.
///
/// For deserializers that support it (currently only `VrlDeserializer`) the
/// template is pre-populated on the synthetic event before any user program
/// executes, making every `%`-prefixed path readable (e.g. `%splunk_hec.host`,
/// `%vector.secrets.*`). For all other deserializers this is a no-op.
Comment thread
thomasqueirozb marked this conversation as resolved.
Outdated
pub fn with_metadata_template(mut self, metadata: EventMetadata) -> Self {
self.deserializer = self.deserializer.with_metadata_template(metadata);
self
}

/// Handles the framing result and parses it into a structured event, if
/// possible.
///
Expand Down
86 changes: 81 additions & 5 deletions lib/codecs/src/decoding/format/vrl.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@ use vrl::{
value::Kind,
};

use crate::{BytesDeserializerConfig, decoding::format::Deserializer};
use vector_core::event::EventMetadata;

use crate::decoding::format::Deserializer;

/// Config used to build a `VrlDeserializer`.
#[configurable_component]
Expand Down Expand Up @@ -64,6 +66,7 @@ impl VrlDeserializerConfig {
Ok(result) => Ok(VrlDeserializer {
program: result.program,
timezone: self.vrl.timezone.unwrap_or(TimeZone::Local),
metadata_template: None,
}),
Err(diagnostics) => Err(Formatter::new(&self.vrl.source, diagnostics)
.to_string()
Expand Down Expand Up @@ -94,9 +97,28 @@ impl VrlDeserializerConfig {
pub struct VrlDeserializer {
program: Program,
timezone: TimeZone,
/// Per-call metadata template set by the source before decoding. When
/// present, every `%`-prefixed path in the template is accessible from
/// within the VRL program (e.g. `%splunk_hec.host`, `%vector.secrets.*`).
metadata_template: Option<EventMetadata>,
}

impl VrlDeserializer {
/// Attach a metadata template that will be pre-populated on each synthetic
/// event before the VRL program runs.
///
/// Sources call this once per decode call with the per-request context they
/// have assembled (envelope fields, auth tokens, etc.). VRL can then read
/// those values via `%`-prefixed paths.
#[must_use]
pub fn with_metadata_template(mut self, metadata: EventMetadata) -> Self {
self.metadata_template = Some(metadata);
self
}
}

fn parse_bytes(bytes: Bytes, log_namespace: LogNamespace) -> Event {
use crate::BytesDeserializerConfig;
let bytes_deserializer = BytesDeserializerConfig::new().build();
let log_event = bytes_deserializer.parse_single(bytes, log_namespace);
Event::from(log_event)
Expand All @@ -108,11 +130,14 @@ impl Deserializer for VrlDeserializer {
bytes: Bytes,
log_namespace: LogNamespace,
) -> vector_common::Result<SmallVec<[Event; 1]>> {
let event = parse_bytes(bytes, log_namespace);
match self.run_vrl(event, log_namespace) {
Ok(events) => Ok(events),
Err(e) => Err(e),
let mut event = parse_bytes(bytes, log_namespace);
if let Some(template) = &self.metadata_template {
// Pre-populate the synthetic event with the source-assembled metadata so
// every `%`-prefixed path is in scope when VRL executes. This lets
// user programs read `%splunk_hec.host`, `%vector.secrets.*`, etc.
*event.metadata_mut() = template.clone();
}
self.run_vrl(event, log_namespace)
}
}

Expand Down Expand Up @@ -320,4 +345,55 @@ mod tests {
.to_string();
assert!(error.contains("aborted"));
}

fn metadata_with_secret(key: &str, value: &str) -> EventMetadata {
let mut metadata = EventMetadata::default();
metadata.secrets_mut().insert(key, value);
metadata
}

/// A VRL program that uses `get_secret!()` can read a secret injected via
/// `with_metadata_template`.
#[test]
fn test_with_metadata_template_vrl_can_read_secret() {
// VRL program copies the injected secret into an event field so we can
// assert on its value. The input bytes become `.message` (Legacy namespace)
// and we add `.secret_value` alongside it.
let decoder = make_decoder(r#".secret_value = get_secret!("my_token")"#)
.with_metadata_template(metadata_with_secret("my_token", "super-secret"));

let bytes = Bytes::from(r#"hello"#);
let events = decoder
.parse(bytes, LogNamespace::Legacy)
.expect("parse should succeed");

assert_eq!(events.len(), 1);
assert_eq!(
*events[0].as_log().get("secret_value").unwrap(),
Value::from("super-secret")
);
}

/// Secrets explicitly set by the VRL program win over the template because
/// `set_secret!` runs after the template is pre-populated.
#[test]
fn test_with_metadata_template_codec_wins_on_collision() {
let decoder = make_decoder(r#"set_secret!("my_token", "codec-wins")"#)
.with_metadata_template(metadata_with_secret("my_token", "template-loses"));

let bytes = Bytes::from(r#"hello"#);
let events = decoder
.parse(bytes, LogNamespace::Legacy)
.expect("parse should succeed");

assert_eq!(
events[0]
.metadata()
.secrets()
.get("my_token")
.unwrap()
.as_ref(),
"codec-wins"
);
}
}
20 changes: 19 additions & 1 deletion lib/codecs/src/decoding/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ use smallvec::SmallVec;
use vector_config::configurable_component;
use vector_core::{
config::{DataType, LogNamespace},
event::Event,
event::{Event, EventMetadata},
schema,
};

Expand Down Expand Up @@ -426,6 +426,12 @@ impl DeserializerConfig {
}
}

/// Returns `true` when this is a VRL deserializer.
/// Sources can use this to decide whether to call `Decoder::with_metadata_template`.
pub fn is_vrl(&self) -> bool {
Comment thread
pront marked this conversation as resolved.
matches!(self, DeserializerConfig::Vrl(_))
}

/// Return the type of event build by this deserializer.
pub fn output_type(&self) -> DataType {
match self {
Expand Down Expand Up @@ -542,6 +548,18 @@ pub enum Deserializer {
Vrl(VrlDeserializer),
}

impl Deserializer {
/// Attaches a metadata template to the inner deserializer, if it supports
/// one. Currently only [`VrlDeserializer`] uses this; for all other variants
/// this is a no-op and `self` is returned unchanged.
Comment thread
thomasqueirozb marked this conversation as resolved.
Outdated
pub fn with_metadata_template(self, metadata: EventMetadata) -> Self {
match self {
Deserializer::Vrl(d) => Deserializer::Vrl(d.with_metadata_template(metadata)),
other => other,
}
}
}

impl format::Deserializer for Deserializer {
fn parse(
&self,
Expand Down
2 changes: 1 addition & 1 deletion lib/vector-core/src/event/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ pub use finalization::{
Finalizable,
};
pub use log_event::LogEvent;
pub use metadata::{DatadogMetricOriginMetadata, EventMetadata, WithMetadata};
pub use metadata::{DatadogMetricOriginMetadata, EventMetadata, Secrets, WithMetadata};
pub use metric::{Metric, MetricKind, MetricTags, MetricValue, StatisticKind};
pub use r#ref::{EventMutRef, EventRef};
use serde::{Deserialize, Serialize};
Expand Down
Loading
Loading