vector/sinks/datadog/events/
config.rs1use http::Uri;
2use indoc::indoc;
3use tower::ServiceBuilder;
4use vector_lib::{config::proxy::ProxyConfig, configurable::configurable_component, schema};
5use vrl::value::Kind;
6
7use super::{
8 service::{DatadogEventsResponse, DatadogEventsService},
9 sink::DatadogEventsSink,
10};
11use crate::{
12 common::datadog,
13 config::{
14 AcknowledgementsConfig, DynValidatedSink, GenerateConfig, Input, SinkConfig, SinkContext,
15 ValidatedSink,
16 },
17 http::HttpClient,
18 sinks::{
19 Healthcheck, VectorSink,
20 datadog::{DatadogCommonConfig, LocalDatadogCommonConfig},
21 util::{
22 HttpEndpoint, ServiceBuilderExt, TowerRequestConfig, TowerRequestSettings,
23 http::{HttpStatusRetryLogic, RetryStrategy},
24 },
25 },
26 tls::MaybeTlsSettings,
27};
28
29#[configurable_component(sink(
31 "datadog_events",
32 "Publish observability events to the Datadog Events API."
33))]
34#[derive(Clone, Debug, Default)]
35#[serde(deny_unknown_fields)]
36pub struct DatadogEventsConfig {
37 #[serde(flatten)]
38 pub dd_common: LocalDatadogCommonConfig,
39
40 #[configurable(derived)]
41 #[serde(default)]
42 pub request: TowerRequestConfig,
43
44 #[configurable(derived)]
45 #[serde(default)]
46 pub retry_strategy: RetryStrategy,
47}
48
49impl GenerateConfig for DatadogEventsConfig {
50 fn generate_config() -> serde_json::Value {
51 serde_yaml::from_str(indoc! {r#"
52 default_api_key: ${DATADOG_API_KEY_ENV_VAR}
53 "#})
54 .unwrap()
55 }
56}
57
58impl DatadogEventsConfig {
59 fn events_endpoint(endpoint: Option<&str>, site: &str) -> crate::Result<Uri> {
61 let base = datadog::get_api_base_endpoint(endpoint, site);
62 Ok(HttpEndpoint::parse(&base)?
63 .append_path("/api/v1/events")?
64 .into_uri())
65 }
66
67 fn build_client(&self, proxy: &ProxyConfig) -> crate::Result<HttpClient> {
68 let tls = MaybeTlsSettings::from_config(self.dd_common.tls.as_ref(), false)?;
69 let client = HttpClient::new(tls, proxy)?;
70 Ok(client)
71 }
72
73 fn build_sink(
74 &self,
75 dd_common: &DatadogCommonConfig,
76 client: HttpClient,
77 validated: &ValidatedEvents,
78 endpoint: Uri,
79 ) -> crate::Result<VectorSink> {
80 let service =
81 DatadogEventsService::new(endpoint, dd_common.default_api_key.clone(), client);
82
83 let request_settings = validated.request_settings.clone();
84 let retry_logic = HttpStatusRetryLogic::new(
85 |req: &DatadogEventsResponse| req.http_status,
86 self.retry_strategy.clone(),
87 );
88
89 let service = ServiceBuilder::new()
90 .settings(request_settings, retry_logic)
91 .service(service);
92
93 let sink = DatadogEventsSink { service };
94
95 Ok(VectorSink::from_event_streamsink(sink))
96 }
97}
98
99#[async_trait::async_trait]
100#[typetag::serde(name = "datadog_events")]
101impl SinkConfig for DatadogEventsConfig {
102 fn input(&self) -> Input {
103 let requirement = schema::Requirement::empty()
104 .required_meaning("message", Kind::bytes())
105 .optional_meaning("host", Kind::bytes())
106 .optional_meaning("timestamp", Kind::timestamp());
107
108 Input::log().with_schema_requirement(requirement)
109 }
110
111 fn acknowledgements(&self) -> &AcknowledgementsConfig {
112 &self.dd_common.acknowledgements
113 }
114
115 fn as_dyn_validated(&self) -> Option<&dyn DynValidatedSink> {
116 Some(self)
117 }
118}
119
120#[derive(Clone, Debug)]
121pub struct ValidatedEvents {
122 request_settings: TowerRequestSettings,
123}
124
125#[async_trait::async_trait]
126impl ValidatedSink for DatadogEventsConfig {
127 type Validated = ValidatedEvents;
128
129 fn validate(&self) -> crate::Result<ValidatedEvents> {
130 let site = self
131 .dd_common
132 .site
133 .clone()
134 .unwrap_or_else(|| datadog::DD_US_SITE.to_owned());
135 let uri = Self::events_endpoint(self.dd_common.endpoint.as_deref(), &site)?;
136 if !matches!(uri.scheme_str(), Some("http" | "https")) || uri.authority().is_none() {
137 return Err("Datadog Events endpoint must be an absolute http(s) URL".into());
138 }
139 let request_settings = self.request.into_settings();
140 Ok(ValidatedEvents { request_settings })
141 }
142
143 async fn build(
144 &self,
145 validated: &ValidatedEvents,
146 cx: SinkContext,
147 ) -> crate::Result<(VectorSink, Healthcheck)> {
148 let client = self.build_client(cx.proxy())?;
149 let global = cx.extra_context.get_or_default::<datadog::Options>();
150 let dd_common = self.dd_common.with_globals(global)?;
151 let healthcheck = dd_common.build_healthcheck(client.clone())?;
152 let endpoint = Self::events_endpoint(dd_common.endpoint.as_deref(), &dd_common.site)?;
153 let sink = self.build_sink(&dd_common, client, validated, endpoint)?;
154
155 Ok((sink, healthcheck))
156 }
157}
158
159#[cfg(test)]
160mod tests {
161 use super::*;
162
163 #[test]
164 fn generate_config() {
165 crate::test_util::test_generate_config::<DatadogEventsConfig>();
166 }
167
168 #[test]
169 fn validate_produces_usable_state() {
170 let config = DatadogEventsConfig {
171 dd_common: LocalDatadogCommonConfig::new(
172 Some("http://127.0.0.1:8080".to_string()),
173 None,
174 None,
175 ),
176 ..Default::default()
177 };
178 config.validate().expect("validation should succeed");
179 assert_eq!(
182 DatadogEventsConfig::events_endpoint(
183 Some("http://127.0.0.1:8080"),
184 datadog::DD_US_SITE,
185 )
186 .expect("endpoint should parse")
187 .to_string(),
188 "http://127.0.0.1:8080/api/v1/events"
189 );
190 }
191
192 #[test]
193 fn validate_rejects_malformed_endpoint() {
194 let config = DatadogEventsConfig {
195 dd_common: LocalDatadogCommonConfig::new(Some("not a uri".to_string()), None, None),
196 ..Default::default()
197 };
198 assert!(config.validate().is_err());
199 }
200
201 #[test]
202 fn validate_accepts_endpoint_without_scheme() {
203 let config = DatadogEventsConfig {
204 dd_common: LocalDatadogCommonConfig::new(
205 Some("localhost:8080".to_string()),
206 None,
207 None,
208 ),
209 ..Default::default()
210 };
211 config.validate().expect("validation should succeed");
212 assert_eq!(
215 DatadogEventsConfig::events_endpoint(Some("localhost:8080"), datadog::DD_US_SITE)
216 .expect("endpoint should parse")
217 .to_string(),
218 "https://localhost:8080/api/v1/events"
219 );
220 }
221}